diff --git a/.github/actions/boot-simulator/action.yml b/.github/actions/boot-simulator/action.yml new file mode 100644 index 0000000..4336d2c --- /dev/null +++ b/.github/actions/boot-simulator/action.yml @@ -0,0 +1,130 @@ +name: Boot iOS Simulator +description: > + Find, boot, and optionally install an app on an iOS simulator. + When force=false (default), picks the closest available device + matching the requested name/version. + +inputs: + device: + description: 'Simulator device name (e.g. "iPhone 16 Pro")' + required: true + ios-version: + description: 'Preferred iOS version (e.g. "18.4"). When force=false the closest available version is used.' + required: false + default: '' + force: + description: 'If true, fail when the exact device + version combo is not found.' + required: false + default: 'false' + app-path: + description: 'Path to .app bundle to install after boot. Skipped if empty.' + required: false + default: '' + +outputs: + udid: + description: 'UDID of the booted simulator' + value: ${{ steps.boot.outputs.udid }} + ios-version: + description: 'Actual iOS version of the booted simulator' + value: ${{ steps.boot.outputs.ios-version }} + +runs: + using: composite + steps: + - name: Boot simulator + id: boot + uses: actions/github-script@v7 + env: + INPUT_DEVICE: ${{ inputs.device }} + INPUT_IOS_VERSION: ${{ inputs.ios-version }} + INPUT_FORCE: ${{ inputs.force }} + INPUT_APP_PATH: ${{ inputs.app-path }} + with: + script: | + const { execSync } = require('child_process'); + + const device = process.env.INPUT_DEVICE; + const wantVersion = process.env.INPUT_IOS_VERSION || ''; + const force = process.env.INPUT_FORCE === 'true'; + const appPath = process.env.INPUT_APP_PATH || ''; + + const raw = execSync('xcrun simctl list devices available --json', { + encoding: 'utf-8', + }); + const { devices } = JSON.parse(raw); + + // Flatten into [{ name, udid, version }] + const candidates = []; + for (const [runtime, list] of Object.entries(devices)) { + const m = runtime.match(/iOS[- ]([\d-]+)/); + if (!m) continue; + const version = m[1].replace(/-/g, '.'); + for (const d of list) { + if (d.name === device && d.isAvailable) { + candidates.push({ name: d.name, udid: d.udid, version }); + } + } + } + + if (candidates.length === 0) { + core.setFailed(`No available simulator found for "${device}"`); + return; + } + + let pick; + + if (wantVersion) { + pick = candidates.find(c => c.version === wantVersion); + if (!pick && force) { + core.setFailed( + `Exact match "${device}" (iOS ${wantVersion}) not found. ` + + `Available: ${candidates.map(c => c.version).join(', ')}` + ); + return; + } + if (!pick) { + // Pick closest version by sorting by distance to requested + const wanted = wantVersion.split('.').map(Number); + candidates.sort((a, b) => { + const va = a.version.split('.').map(Number); + const vb = b.version.split('.').map(Number); + const distA = Math.abs(va[0] - wanted[0]) * 1000 + Math.abs((va[1] || 0) - (wanted[1] || 0)); + const distB = Math.abs(vb[0] - wanted[0]) * 1000 + Math.abs((vb[1] || 0) - (wanted[1] || 0)); + return distA - distB; + }); + pick = candidates[0]; + core.warning( + `iOS ${wantVersion} not available for "${device}", ` + + `using iOS ${pick.version} instead` + ); + } + } else { + // No version preference β€” pick the latest + candidates.sort((a, b) => { + const va = a.version.split('.').map(Number); + const vb = b.version.split('.').map(Number); + return (vb[0] - va[0]) * 1000 + (vb[1] || 0) - (va[1] || 0); + }); + pick = candidates[0]; + } + + core.info(`Booting ${pick.name} (iOS ${pick.version}) β€” ${pick.udid}`); + try { + execSync(`xcrun simctl boot "${pick.udid}"`, { stdio: 'inherit' }); + } catch { + core.info('Simulator already booted or boot returned non-zero (continuing)'); + } + + core.info('Waiting for simulator to finish booting…'); + execSync(`xcrun simctl bootstatus "${pick.udid}" -b`, { stdio: 'inherit' }); + + if (appPath) { + core.info(`Installing ${appPath}`); + execSync(`xcrun simctl install "${pick.udid}" "${appPath}"`, { + stdio: 'inherit', + }); + } + + core.setOutput('udid', pick.udid); + core.setOutput('ios-version', pick.version); diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..3d92e71 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,36 @@ +name: Setup environment +description: Install asdf tools and run bun install (repo must already be checked out) + +inputs: + java-version: + description: 'JDK version to install (skipped if empty)' + required: false + default: '' + +runs: + using: composite + steps: + - name: Install asdf + uses: asdf-vm/actions/setup@v4 + + - name: Tools cache + id: asdf-cache + uses: actions/cache@v4 + with: + path: ~/.asdf/ + key: ${{ runner.os }}-${{ hashFiles('**/.tool-versions') }} + + - name: Install tools from .tool-versions + if: steps.asdf-cache.outputs.cache-hit != 'true' + uses: asdf-vm/actions/install@v4 + + - name: Set up JDK + if: inputs.java-version != '' + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: ${{ inputs.java-version }} + + - name: Install dependencies + shell: bash + run: bun install --frozen-lockfile diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5579459..20f8ab7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,26 +21,15 @@ on: - '**/README*' workflow_dispatch: +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install asdf - uses: asdf-vm/actions/setup@v4 - - - name: Tools cache - id: asdf-cache - uses: actions/cache@v4 - with: - path: ~/.asdf/ - key: ${{ runner.os }}-${{ hashFiles('**/.tool-versions') }} - - - name: Install tools from .tool-versions - if: steps.asdf-cache.outputs.cache-hit != 'true' - uses: asdf-vm/actions/install@v4 + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup - name: Install ktlint run: | @@ -48,9 +37,6 @@ jobs: chmod +x ktlint sudo mv ktlint /usr/local/bin/ - - name: Install dependencies - run: bun install --frozen-lockfile - - name: Run ESLint run: bun run lint @@ -61,25 +47,8 @@ jobs: test: runs-on: macos-15 steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install asdf - uses: asdf-vm/actions/setup@v4 - - - name: Tools cache - id: asdf-cache - uses: actions/cache@v4 - with: - path: ~/.asdf/ - key: ${{ runner.os }}-${{ hashFiles('**/.tool-versions') }} - - - name: Install tools from .tool-versions - if: steps.asdf-cache.outputs.cache-hit != 'true' - uses: asdf-vm/actions/install@v4 - - - name: Install dependencies - run: bun install --frozen-lockfile + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup - name: Run Test run: bun run test @@ -89,27 +58,13 @@ jobs: needs: [lint, test] strategy: matrix: - example: [demo] # side-by-side, recursive, fs-experiment + example: [demo] concurrency: group: '${{ github.workflow }}-ios-${{ matrix.example }}-${{ github.head_ref || github.ref_name }}' cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install asdf - uses: asdf-vm/actions/setup@v4 - - - name: Tools cache - id: asdf-cache - uses: actions/cache@v4 - with: - path: ~/.asdf/ - key: ${{ runner.os }}-${{ hashFiles('**/.tool-versions') }} - - - name: Install tools - if: steps.asdf-cache.outputs.cache-hit != 'true' - uses: asdf-vm/actions/install@v4 + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup - name: Setup ccache uses: hendrikmuhs/ccache-action@v1.2 @@ -122,9 +77,6 @@ jobs: echo "/opt/homebrew/opt/ccache/libexec" >> $GITHUB_PATH echo "CCACHE_LOGFILE=/tmp/ccache.log" >> $GITHUB_ENV - - name: Install dependencies - run: bun install --frozen-lockfile - - name: Install example dependencies working-directory: apps/${{ matrix.example }} run: | @@ -137,8 +89,7 @@ jobs: - name: Build iOS working-directory: apps/${{ matrix.example }} - run: | - npx react-native build-ios --mode Debug + run: npx react-native build-ios --mode Debug - name: ccache stats run: | @@ -156,32 +107,11 @@ jobs: group: '${{ github.workflow }}-android-${{ matrix.example }}-${{ github.head_ref || github.ref_name }}' cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install asdf - uses: asdf-vm/actions/setup@v4 - - - name: Tools cache - id: asdf-cache - uses: actions/cache@v4 + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup with: - path: ~/.asdf/ - key: ${{ runner.os }}-${{ hashFiles('**/.tool-versions') }} - - - name: Install tools - if: steps.asdf-cache.outputs.cache-hit != 'true' - uses: asdf-vm/actions/install@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' java-version: '17' - - name: Install dependencies - run: bun install --frozen-lockfile - - name: Install example dependencies working-directory: apps/${{ matrix.example }} run: bun install --frozen-lockfile @@ -189,3 +119,94 @@ jobs: - name: Build Android working-directory: apps/${{ matrix.example }} run: npx react-native build-android --mode Debug + + harness-ios: + runs-on: macos-15 + needs: [lint, test] + timeout-minutes: 45 + concurrency: + group: '${{ github.workflow }}-harness-ios-${{ github.head_ref || github.ref_name }}' + cancel-in-progress: true + env: + HARNESS_IOS_DEVICE: iPhone 16 Pro + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + + - name: Setup ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: harness-ios + max-size: 2G + + - name: Setup ccache path + run: echo "/opt/homebrew/opt/ccache/libexec" >> $GITHUB_PATH + + - name: Install CocoaPods + working-directory: apps/fs-experiment/ios + run: pod install + + - name: Build iOS + working-directory: apps/fs-experiment + run: npx react-native build-ios --mode Debug + + - name: Find built app + id: app-path + run: | + APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData \ + -name "MultInstance-FSExperiment.app" \ + -path "*/Debug-iphonesimulator/*" \ + | head -1) + echo "path=$APP_PATH" >> "$GITHUB_OUTPUT" + + - name: Boot simulator & install app + id: simulator + uses: ./.github/actions/boot-simulator + with: + device: ${{ env.HARNESS_IOS_DEVICE }} + app-path: ${{ steps.app-path.outputs.path }} + + - name: Run harness tests (iOS) + working-directory: apps/fs-experiment + env: + HARNESS_IOS_VERSION: ${{ steps.simulator.outputs.ios-version }} + run: bun run harness:ios + + harness-android: + runs-on: ubuntu-latest + needs: [lint, test] + timeout-minutes: 45 + concurrency: + group: '${{ github.workflow }}-harness-android-${{ github.head_ref || github.ref_name }}' + cancel-in-progress: true + env: + HARNESS_ANDROID_DEVICE: test_device + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + java-version: '17' + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Build Android APK + working-directory: apps/fs-experiment + run: npx react-native build-android --tasks assembleDebug + + - name: Run emulator and harness tests + uses: ReactiveCircus/android-emulator-runner@v2 + with: + api-level: 35 + target: google_apis + arch: x86_64 + avd-name: test_device + force-avd-creation: true + emulator-options: -no-snapshot-save -no-window -noaudio -no-boot-anim + disable-animations: true + script: | + adb install apps/fs-experiment/android/app/build/outputs/apk/debug/app-debug.apk + cd apps/fs-experiment && bun harness:android diff --git a/EXPO_SUPPORT_IMPLEMENTATION.md b/EXPO_SUPPORT_IMPLEMENTATION.md new file mode 100644 index 0000000..cffefbc --- /dev/null +++ b/EXPO_SUPPORT_IMPLEMENTATION.md @@ -0,0 +1,279 @@ +# Expo Support Implementation for react-native-sandbox + +## Overview + +This document describes the implementation of Expo support for the `@callstack/react-native-sandbox` package. The implementation uses conditional compilation to support both React Native CLI and Expo environments seamlessly. + +## Architecture + +### Problem Statement + +The original `react-native-sandbox` package heavily depends on React Native's `RCTDefaultReactNativeFactoryDelegate` and `RCTReactNativeFactory` classes. Expo uses different factory classes (`ExpoReactNativeFactory` and `ExpoReactNativeFactoryDelegate`) that are not compatible with the React Native CLI classes. + +### Solution + +The solution implements **conditional compilation** using preprocessor definitions to switch between React Native and Expo classes at compile time. This approach: + +1. **Maintains the same API** - No changes required in JavaScript code +2. **Uses drop-in replacement** - Expo classes are used when `EXPO_MODULE` is defined +3. **Preserves functionality** - All sandbox features work in both environments +4. **Ensures compatibility** - Works with both React Native CLI and Expo projects + +## Implementation Details + +### 1. Conditional Header Imports + +**File**: `packages/react-native-sandbox/ios/SandboxReactNativeDelegate.h` + +```objc +// Conditional imports based on platform +#ifdef EXPO_MODULE +// Expo imports +#import +#else +// React Native imports +#import +#endif + +// Conditional class inheritance +#ifdef EXPO_MODULE +@interface SandboxReactNativeDelegate : ExpoReactNativeFactoryDelegate +#else +@interface SandboxReactNativeDelegate : RCTDefaultReactNativeFactoryDelegate +#endif +``` + +### 2. Conditional Implementation + +**File**: `packages/react-native-sandbox/ios/SandboxReactNativeDelegate.mm` + +```objc +// Conditional imports for Expo support +#ifdef EXPO_MODULE +#import +#import +#endif + +// Conditional initialization +- (instancetype)init +{ + if (self = [super init]) { + _hasOnMessageHandler = NO; + _hasOnErrorHandler = NO; + +#ifdef EXPO_MODULE + // Expo-specific initialization + NSLog(@"[SandboxReactNativeDelegate] Initialized for Expo environment"); +#else + // React Native initialization + self.dependencyProvider = [[RCTAppDependencyProvider alloc] init]; +#endif + } + return self; +} + +// Conditional bundle URL handling +- (NSURL *)bundleURL +{ + // ... common code ... + +#ifdef EXPO_MODULE + // Expo-specific bundle URL handling + NSString *bundleName = [jsBundleSourceNS hasSuffix:@".bundle"] ? + [jsBundleSourceNS stringByDeletingPathExtension] : jsBundleSourceNS; + return [[NSBundle mainBundle] URLForResource:bundleName withExtension:@"bundle"]; +#else + // React Native bundle URL handling + NSString *bundleName = [jsBundleSourceNS hasSuffix:@".bundle"] ? + [jsBundleSourceNS stringByDeletingPathExtension] : jsBundleSourceNS; + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:bundleName]; +#endif +} +``` + +### 3. Conditional Component View + +**File**: `packages/react-native-sandbox/ios/SandboxReactNativeViewComponentView.mm` + +```objc +// Conditional imports based on platform +#ifdef EXPO_MODULE +#import +#else +#import +#endif + +// Conditional property declaration +@interface SandboxReactNativeViewComponentView () +#ifdef EXPO_MODULE +@property (nonatomic, strong) ExpoReactNativeFactory *reactNativeFactory; +#else +@property (nonatomic, strong) RCTReactNativeFactory *reactNativeFactory; +#endif +// ... other properties +@end + +// Conditional factory creation +- (void)loadReactNativeView +{ + // ... common code ... + + if (!self.reactNativeFactory) { +#ifdef EXPO_MODULE + self.reactNativeFactory = [[ExpoReactNativeFactory alloc] initWithDelegate:self.reactNativeDelegate]; +#else + self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self.reactNativeDelegate]; +#endif + } + + // ... rest of the method +} +``` + +### 4. Conditional Podspec Configuration + +**File**: `packages/react-native-sandbox/React-Sandbox.podspec` + +```ruby +# Add Expo-specific header search paths when building for Expo +if ENV['EXPO_MODULE'] == '1' + header_search_paths << "\"$(PODS_ROOT)/Headers/Public/ExpoModulesCore\"" +end + +# Conditional dependencies based on platform +if ENV['EXPO_MODULE'] == '1' + s.dependency "ExpoModulesCore" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => header_search_paths, + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", + "GCC_PREPROCESSOR_DEFINITIONS" => "EXPO_MODULE=1" + } +else + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => header_search_paths, + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } +end +``` + +## Usage + +### React Native CLI Projects + +No changes required. The package works as before: + +```tsx +import SandboxReactNativeView from '@callstack/react-native-sandbox'; + + +``` + +### Expo Projects + +1. **Install the package**: + ```bash + npx expo install @callstack/react-native-sandbox + ``` + +2. **Use the same API**: + ```tsx + import SandboxReactNativeView from '@callstack/react-native-sandbox'; + + + ``` + +3. **Optional configuration** in `app.json`: + ```json + { + "expo": { + "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "useFrameworks": "static" + } + } + ] + ] + } + } + ``` + +## Demo App + +A complete Expo demo app is provided at `apps/expo-demo/` that demonstrates: + +- **Counter App**: Simple counter with increment/decrement functionality +- **Calculator App**: Basic calculator with arithmetic operations +- **Sandboxed Environment**: Each app runs in its own isolated React Native instance +- **Message Passing**: Apps can communicate through the sandbox messaging system + +### Running the Demo + +```bash +cd apps/expo-demo +npm install +npm start +``` + +## Key Benefits + +1. **Seamless Integration**: No code changes required when switching between React Native CLI and Expo +2. **Drop-in Replacement**: Expo classes are used automatically when detected +3. **Full Feature Parity**: All sandbox features work identically in both environments +4. **Backward Compatibility**: Existing React Native CLI projects continue to work unchanged +5. **Future-Proof**: Easy to extend for other React Native environments + +## Technical Considerations + +### Preprocessor Definitions + +The implementation uses `EXPO_MODULE` as the main preprocessor definition to distinguish between environments. This is set automatically by the podspec when building for Expo. + +### Bundle URL Handling + +Expo and React Native CLI handle bundle URLs differently: +- **React Native CLI**: Uses `RCTBundleURLProvider` for development and production bundles +- **Expo**: Uses direct bundle file access from the app bundle + +### Dependency Provider + +Expo may handle dependency providers differently than React Native CLI, so the initialization is conditional. + +### Factory Classes + +The core difference is in the factory classes: +- **React Native CLI**: `RCTReactNativeFactory` and `RCTDefaultReactNativeFactoryDelegate` +- **Expo**: `ExpoReactNativeFactory` and `ExpoReactNativeFactoryDelegate` + +## Testing + +The implementation has been tested with: + +1. **React Native CLI projects**: All existing functionality preserved +2. **Expo projects**: Full sandbox functionality working +3. **Cross-platform**: iOS and Android support maintained +4. **Message passing**: Inter-sandbox communication working +5. **Error handling**: Proper error propagation in both environments + +## Future Enhancements + +1. **Android Support**: Extend the same conditional compilation approach to Android +2. **Additional Expo Features**: Leverage Expo-specific features when available +3. **Performance Optimization**: Optimize for Expo's specific runtime characteristics +4. **Plugin System**: Create an Expo plugin for easier integration + +## Conclusion + +The Expo support implementation successfully provides a seamless experience for both React Native CLI and Expo developers. The conditional compilation approach ensures that the package works optimally in both environments while maintaining a consistent API and full feature parity. \ No newline at end of file diff --git a/README.md b/README.md index 3d578bc..aadd39a 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ Full examples: - [`apps/recursive`](./apps/recursive/README.md): An example application with few nested sandbox instances. - [`apps/p2p-chat`](./apps/p2p-counter/README.md): Direct sandbox-to-sandbox chat demo. - [`apps/p2p-counter`](./apps/p2p-counter/README.md): Direct sandbox-to-sandbox communication demo. +- [`apps/fs-experiment`](./apps/fs-experiment/README.md): File system & storage isolation with TurboModule substitutions. ## πŸ“š API Reference @@ -170,9 +171,15 @@ We're actively working on expanding the capabilities of `react-native-sandbox`. - Resource usage limits and monitoring - Sandbox capability restrictions - Unresponsiveness detection -- [ ] **Storage Isolation** - Secure data partitioning - - Per-sandbox AsyncStorage isolation - - Secure file system access controls +- [x] **TurboModule Substitutions** - Replace native module implementations per sandbox + - Configurable via `turboModuleSubstitutions` prop (JS/TS only) + - Sandbox-aware modules receive origin context for per-instance scoping + - TurboModules (New Architecture / Fabric) only +- [x] **Storage & File System Isolation** - Secure data partitioning + - Per-sandbox AsyncStorage isolation via scoped storage directories + - Sandboxed file system access (react-native-fs, react-native-file-access) with path jailing + - All directory constants overridden to sandbox-scoped paths + - Network/system operations blocked in sandboxed FS modules - [ ] **Developer Tools** - Enhanced debugging and development experience Contributions and feedback on these roadmap items are welcome! Please check our [issues](https://github.com/callstackincubator/react-native-sandbox/issues) for detailed discussions on each feature. @@ -187,9 +194,24 @@ A primary security concern when running multiple React Native instances is the p - **Data Leakage:** One sandbox could use a shared TurboModule to store data, which could then be read by another sandbox or the host. This breaks the isolation model. - **Unintended Side-Effects:** A sandbox could call a method on a shared module that changes its state, affecting the behavior of the host or other sandboxes in unpredictable ways. -To address this, `react-native-sandbox` allows you to provide a **whitelist of allowed TurboModules** for each sandbox instance via the `allowedTurboModules` prop. Only the modules specified in this list will be accessible from within the sandbox, significantly reducing the attack surface. It is critical to only whitelist modules that are stateless or are explicitly designed to be shared safely. +To address this, `react-native-sandbox` provides two mechanisms: -**Default Whitelist:** By default, only `NativeMicrotasksCxx` is whitelisted. Modules like `NativePerformanceCxx`, `PlatformConstants`, `DevSettings`, `LogBox`, and other third-party modules are *not* whitelisted. +- **TurboModule Allowlisting** β€” Use the `allowedTurboModules` prop to control which native modules the sandbox can access. Only modules in this list are resolved; all others return a stub that rejects with a clear error. + +- **TurboModule Substitutions** β€” Use the `turboModuleSubstitutions` prop to transparently replace a module with a sandbox-aware alternative. For example, when sandbox JS requests `RNCAsyncStorage`, the host can resolve different implementation like `SandboxedAsyncStorage` instead β€” an implementation that scopes storage to the sandbox's origin. Substituted modules that conform to `RCTSandboxAwareModule` (ObjC) or `ISandboxAwareModule` (C++) receive the sandbox context (origin, requested name, resolved name) after instantiation. + +```tsx + +``` + +**Default Allowlist:** A baseline set of essential React Native modules is allowed by default (e.g., `EventDispatcher`, `AppState`, `Appearance`, `Networking`, `DeviceInfo`, `KeyboardObserver`, and others required for basic rendering and dev tooling). See the [full list in source](https://github.com/callstackincubator/react-native-sandbox/blob/main/packages/react-native-sandbox/src/index.tsx). Third-party modules and storage/FS modules are *not* included β€” they must be explicitly added via `allowedTurboModules` or provided through `turboModuleSubstitutions`. ### Performance @@ -197,7 +219,11 @@ To address this, `react-native-sandbox` allows you to provide a **whitelist of a ### File System & Storage -- **Persistent Storage Conflicts:** Standard APIs like `AsyncStorage` may not be instance-aware, potentially allowing a sandbox to read or overwrite data stored by the host or other sandboxes. Any storage mechanism must be properly partitioned to ensure data isolation. +- **Persistent Storage Conflicts:** Standard APIs like `AsyncStorage` are not instance-aware by default, potentially allowing a sandbox to read or overwrite data stored by the host or other sandboxes. Use `turboModuleSubstitutions` to replace these modules with sandbox-aware implementations that scope data per origin. +- **File System Path Jailing:** Sandboxed file system modules (`SandboxedRNFSManager`, `SandboxedFileAccess`) override directory constants and validate all path arguments, ensuring file operations are confined to a per-origin sandbox directory. Paths outside the sandbox root are rejected with `EPERM`. +- **Network Operations Blocked:** Sandboxed FS modules block download/upload/fetch operations to prevent data exfiltration. + +See the [`apps/fs-experiment`](./apps/fs-experiment/) example for a working demonstration. ### Platform-Specific Considerations diff --git a/apps/expo/android/app/src/main/java/com/callstack/expodemo/MainActivity.kt b/apps/expo/android/app/src/main/java/com/callstack/expodemo/MainActivity.kt new file mode 100644 index 0000000..6e6d01b --- /dev/null +++ b/apps/expo/android/app/src/main/java/com/callstack/expodemo/MainActivity.kt @@ -0,0 +1,61 @@ +package com.callstack.expodemo + +import android.os.Build +import android.os.Bundle + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +import expo.modules.ReactActivityDelegateWrapper + +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + setTheme(R.style.AppTheme); + super.onCreate(null) + } + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "main" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate { + return ReactActivityDelegateWrapper( + this, + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + object : DefaultReactActivityDelegate( + this, + mainComponentName, + fabricEnabled + ){}) + } + + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + * @see onBackPressed + */ + override fun invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed() + } + return + } + + // Use the default back button implementation on Android S + // because it's doing more than [Activity.moveTaskToBack] in fact. + super.invokeDefaultOnBackPressed() + } +} diff --git a/apps/expo/android/app/src/main/java/com/callstack/expodemo/MainApplication.kt b/apps/expo/android/app/src/main/java/com/callstack/expodemo/MainApplication.kt new file mode 100644 index 0000000..d61fe8d --- /dev/null +++ b/apps/expo/android/app/src/main/java/com/callstack/expodemo/MainApplication.kt @@ -0,0 +1,57 @@ +package com.callstack.expodemo + +import android.app.Application +import android.content.res.Configuration + +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.ReactHost +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.soloader.OpenSourceMergedSoMapping +import com.facebook.soloader.SoLoader + +import expo.modules.ApplicationLifecycleDispatcher +import expo.modules.ReactNativeHostWrapper + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( + this, + object : DefaultReactNativeHost(this) { + override fun getPackages(): List { + val packages = PackageList(this).packages + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(MyReactNativePackage()) + return packages + } + + override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + ) + + override val reactHost: ReactHost + get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + SoLoader.init(this, OpenSourceMergedSoMapping) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + ApplicationLifecycleDispatcher.onApplicationCreate(this) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) + } +} diff --git a/apps/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..3941bea --- /dev/null +++ b/apps/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..3941bea --- /dev/null +++ b/apps/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/apps/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..ac03dbf Binary files /dev/null and b/apps/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/apps/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/apps/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..e1173a9 Binary files /dev/null and b/apps/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/apps/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/apps/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..ff086fd Binary files /dev/null and b/apps/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/apps/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/apps/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..f7f1d06 Binary files /dev/null and b/apps/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/apps/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/apps/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..49a464e Binary files /dev/null and b/apps/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/apps/expo/android/app/src/main/res/values-night/colors.xml b/apps/expo/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..3c05de5 --- /dev/null +++ b/apps/expo/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/expo/ios/ExpoDemo.xcodeproj/project.pbxproj b/apps/expo/ios/ExpoDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..123a790 --- /dev/null +++ b/apps/expo/ios/ExpoDemo.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 15029D433FBC71CA55B1AAFD /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37196DCD2C7AC11D48A1D3DD /* ExpoModulesProvider.swift */; }; + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 4B21E2A9D7B6A2E8666F18A5 /* Pods_ExpoDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7DE1F0D4DD2CF1D79191460 /* Pods_ExpoDemo.framework */; }; + A1E6A914A06C76F306ABF9B3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0A6657DC84DD239345670896 /* PrivacyInfo.xcprivacy */; }; + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0A6657DC84DD239345670896 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = ExpoDemo/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* ExpoDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExpoDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ExpoDemo/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ExpoDemo/Info.plist; sourceTree = ""; }; + 37196DCD2C7AC11D48A1D3DD /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-ExpoDemo/ExpoModulesProvider.swift"; sourceTree = ""; }; + 9B8CBD7285A315AAD19C4AB0 /* Pods-ExpoDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ExpoDemo.debug.xcconfig"; path = "Target Support Files/Pods-ExpoDemo/Pods-ExpoDemo.debug.xcconfig"; sourceTree = ""; }; + A7DE1F0D4DD2CF1D79191460 /* Pods_ExpoDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ExpoDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = ExpoDemo/SplashScreen.storyboard; sourceTree = ""; }; + BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + EF7C071F9A95C46154493456 /* Pods-ExpoDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ExpoDemo.release.xcconfig"; path = "Target Support Files/Pods-ExpoDemo/Pods-ExpoDemo.release.xcconfig"; sourceTree = ""; }; + F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ExpoDemo/AppDelegate.swift; sourceTree = ""; }; + F11748442D0722820044C1D9 /* ExpoDemo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "ExpoDemo-Bridging-Header.h"; path = "ExpoDemo/ExpoDemo-Bridging-Header.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B21E2A9D7B6A2E8666F18A5 /* Pods_ExpoDemo.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* ExpoDemo */ = { + isa = PBXGroup; + children = ( + F11748412D0307B40044C1D9 /* AppDelegate.swift */, + F11748442D0722820044C1D9 /* ExpoDemo-Bridging-Header.h */, + BB2F792B24A3F905000567C9 /* Supporting */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + 0A6657DC84DD239345670896 /* PrivacyInfo.xcprivacy */, + ); + name = ExpoDemo; + sourceTree = ""; + }; + 18AA523085709E3B734ABF8C /* ExpoDemo */ = { + isa = PBXGroup; + children = ( + 37196DCD2C7AC11D48A1D3DD /* ExpoModulesProvider.swift */, + ); + name = ExpoDemo; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + A7DE1F0D4DD2CF1D79191460 /* Pods_ExpoDemo.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6EA69F7D5BB8B70C86E8D5D9 /* Pods */ = { + isa = PBXGroup; + children = ( + 9B8CBD7285A315AAD19C4AB0 /* Pods-ExpoDemo.debug.xcconfig */, + EF7C071F9A95C46154493456 /* Pods-ExpoDemo.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* ExpoDemo */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + 6EA69F7D5BB8B70C86E8D5D9 /* Pods */, + F916C15C4676FDF885902ABB /* ExpoModulesProviders */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* ExpoDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + BB2F792B24A3F905000567C9 /* Supporting */ = { + isa = PBXGroup; + children = ( + BB2F792C24A3F905000567C9 /* Expo.plist */, + ); + name = Supporting; + path = ExpoDemo/Supporting; + sourceTree = ""; + }; + F916C15C4676FDF885902ABB /* ExpoModulesProviders */ = { + isa = PBXGroup; + children = ( + 18AA523085709E3B734ABF8C /* ExpoDemo */, + ); + name = ExpoModulesProviders; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* ExpoDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ExpoDemo" */; + buildPhases = ( + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, + FBBB988E809FD658C6E42D79 /* [Expo] Configure project */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, + D043A690E691750B2E092D66 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ExpoDemo; + productName = ExpoDemo; + productReference = 13B07F961A680F5B00A75B9A /* ExpoDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1130; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ExpoDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* ExpoDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + A1E6A914A06C76F306ABF9B3 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; + }; + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ExpoDemo-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ExpoDemo/Pods-ExpoDemo-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpoDemo/Pods-ExpoDemo-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + D043A690E691750B2E092D66 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ExpoDemo/Pods-ExpoDemo-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpoDemo/Pods-ExpoDemo-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FBBB988E809FD658C6E42D79 /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env", + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/ExpoDemo/ExpoDemo.entitlements", + "$(SRCROOT)/Pods/Target Support Files/Pods-ExpoDemo/expo-configure-project.sh", + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Pods/Target Support Files/Pods-ExpoDemo/ExpoModulesProvider.swift", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-ExpoDemo/expo-configure-project.sh\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, + 15029D433FBC71CA55B1AAFD /* ExpoModulesProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9B8CBD7285A315AAD19C4AB0 /* Pods-ExpoDemo.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = ExpoDemo/ExpoDemo.entitlements; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FB_SONARKIT_ENABLED=1", + ); + INFOPLIST_FILE = ExpoDemo/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.callstack.expodemo; + PRODUCT_NAME = ExpoDemo; + SWIFT_OBJC_BRIDGING_HEADER = "ExpoDemo/ExpoDemo-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EF7C071F9A95C46154493456 /* Pods-ExpoDemo.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = ExpoDemo/ExpoDemo.entitlements; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = ExpoDemo/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.callstack.expodemo; + PRODUCT_NAME = ExpoDemo; + SWIFT_OBJC_BRIDGING_HEADER = "ExpoDemo/ExpoDemo-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios", + ); + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + USE_HERMES = true; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios", + ); + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + USE_HERMES = true; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ExpoDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ExpoDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/apps/expo/ios/ExpoDemo.xcodeproj/xcshareddata/xcschemes/ExpoDemo.xcscheme b/apps/expo/ios/ExpoDemo.xcodeproj/xcshareddata/xcschemes/ExpoDemo.xcscheme new file mode 100644 index 0000000..6025757 --- /dev/null +++ b/apps/expo/ios/ExpoDemo.xcodeproj/xcshareddata/xcschemes/ExpoDemo.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/expo/ios/ExpoDemo.xcworkspace/contents.xcworkspacedata b/apps/expo/ios/ExpoDemo.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..d5f98c0 --- /dev/null +++ b/apps/expo/ios/ExpoDemo.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/expo/ios/ExpoDemo/AppDelegate.swift b/apps/expo/ios/ExpoDemo/AppDelegate.swift new file mode 100644 index 0000000..a7887e1 --- /dev/null +++ b/apps/expo/ios/ExpoDemo/AppDelegate.swift @@ -0,0 +1,70 @@ +import Expo +import React +import ReactAppDependencyProvider + +@UIApplicationMain +public class AppDelegate: ExpoAppDelegate { + var window: UIWindow? + + var reactNativeDelegate: ExpoReactNativeFactoryDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + let delegate = ReactNativeDelegate() + let factory = ExpoReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + bindReactNativeFactory(factory) + +#if os(iOS) || os(tvOS) + window = UIWindow(frame: UIScreen.main.bounds) + factory.startReactNative( + withModuleName: "main", + in: window, + launchOptions: launchOptions) +#endif + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + // Linking API + public override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + } + + // Universal Links + public override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } +} + +class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { + // Extension point for config-plugins + + override func sourceURL(for bridge: RCTBridge) -> URL? { + // needed to return the correct URL for expo-dev-client. + bridge.bundleURL ?? bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") +#else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/apps/expo/ios/ExpoDemo/ExpoDemo-Bridging-Header.h b/apps/expo/ios/ExpoDemo/ExpoDemo-Bridging-Header.h new file mode 100644 index 0000000..8361941 --- /dev/null +++ b/apps/expo/ios/ExpoDemo/ExpoDemo-Bridging-Header.h @@ -0,0 +1,3 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// diff --git a/apps/expo/ios/ExpoDemo/ExpoDemo.entitlements b/apps/expo/ios/ExpoDemo/ExpoDemo.entitlements new file mode 100644 index 0000000..f683276 --- /dev/null +++ b/apps/expo/ios/ExpoDemo/ExpoDemo.entitlements @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/expo/ios/ExpoDemo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/apps/expo/ios/ExpoDemo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 0000000..2732229 Binary files /dev/null and b/apps/expo/ios/ExpoDemo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/apps/expo/ios/ExpoDemo/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/expo/ios/ExpoDemo/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..90d8d4c --- /dev/null +++ b/apps/expo/ios/ExpoDemo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": [ + { + "filename": "App-Icon-1024x1024@1x.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/apps/expo/ios/ExpoDemo/Images.xcassets/Contents.json b/apps/expo/ios/ExpoDemo/Images.xcassets/Contents.json new file mode 100644 index 0000000..ed285c2 --- /dev/null +++ b/apps/expo/ios/ExpoDemo/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "expo" + } +} diff --git a/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenBackground.colorset/Contents.json new file mode 100644 index 0000000..15f02ab --- /dev/null +++ b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors": [ + { + "color": { + "components": { + "alpha": "1.000", + "blue": "1.00000000000000", + "green": "1.00000000000000", + "red": "1.00000000000000" + }, + "color-space": "srgb" + }, + "idiom": "universal" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/Contents.json b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/Contents.json new file mode 100644 index 0000000..f65c008 --- /dev/null +++ b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "image@2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "image@3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image.png b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image.png new file mode 100644 index 0000000..b9ff0fc Binary files /dev/null and b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image.png differ diff --git a/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png new file mode 100644 index 0000000..b9ff0fc Binary files /dev/null and b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png differ diff --git a/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png new file mode 100644 index 0000000..b9ff0fc Binary files /dev/null and b/apps/expo/ios/ExpoDemo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png differ diff --git a/apps/expo/ios/ExpoDemo/Info.plist b/apps/expo/ios/ExpoDemo/Info.plist new file mode 100644 index 0000000..6677caa --- /dev/null +++ b/apps/expo/ios/ExpoDemo/Info.plist @@ -0,0 +1,76 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Expo Demo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + com.callstack.expodemo + + + + CFBundleVersion + 1 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + RCTNewArchEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Light + UIViewControllerBasedStatusBarAppearance + + + diff --git a/apps/expo/ios/ExpoDemo/PrivacyInfo.xcprivacy b/apps/expo/ios/ExpoDemo/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..5bb83c5 --- /dev/null +++ b/apps/expo/ios/ExpoDemo/PrivacyInfo.xcprivacy @@ -0,0 +1,48 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + 0A2A.1 + 3B52.1 + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + 85F4.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/apps/expo/ios/ExpoDemo/SplashScreen.storyboard b/apps/expo/ios/ExpoDemo/SplashScreen.storyboard new file mode 100644 index 0000000..8a6fcd4 --- /dev/null +++ b/apps/expo/ios/ExpoDemo/SplashScreen.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/expo/ios/ExpoDemo/Supporting/Expo.plist b/apps/expo/ios/ExpoDemo/Supporting/Expo.plist new file mode 100644 index 0000000..750be02 --- /dev/null +++ b/apps/expo/ios/ExpoDemo/Supporting/Expo.plist @@ -0,0 +1,12 @@ + + + + + EXUpdatesCheckOnLaunch + ALWAYS + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + + \ No newline at end of file diff --git a/apps/fs-experiment/App.tsx b/apps/fs-experiment/App.tsx index 8e6d95c..50c6ae2 100644 --- a/apps/fs-experiment/App.tsx +++ b/apps/fs-experiment/App.tsx @@ -2,222 +2,131 @@ import SandboxReactNativeView from '@callstack/react-native-sandbox' import React, {useState} from 'react' import { Platform, + Pressable, SafeAreaView, ScrollView, StatusBar, StyleSheet, + Switch, Text, - TextInput, - TouchableOpacity, useColorScheme, View, } from 'react-native' -// File system imports -import RNFS from 'react-native-fs' -const SHARED_FILE_PATH = `${RNFS.DocumentDirectoryPath}/shared_test_file.txt` +import FileOpsUI from './FileOpsUI' + +// iOS async-storage exposes two module names: the legacy bridge name +// (PlatformLocalStorage) and the TurboModule spec name (RNC_AsyncSQLiteDBStorage). +// Both must be allowed so the sandbox can access the real module when +// substitution is OFF, and so the substitution mapping works when ON. +const ASYNC_STORAGE_MODULES = + Platform.OS === 'ios' + ? ['RNC_AsyncSQLiteDBStorage', 'PlatformLocalStorage', 'RNCAsyncStorage'] + : ['RNCAsyncStorage'] + +const ALL_TURBO_MODULES = + Platform.OS === 'ios' + ? ['RNFSManager', 'FileAccess', ...ASYNC_STORAGE_MODULES] + : ['FileAccess', ...ASYNC_STORAGE_MODULES] + +const SANDBOXED_SUBSTITUTIONS: Record = { + ...(Platform.OS === 'ios' && {RNFSManager: 'SandboxedRNFSManager'}), + FileAccess: 'SandboxedFileAccess', + ...Object.fromEntries( + ASYNC_STORAGE_MODULES.map(m => [m, 'SandboxedAsyncStorage']) + ), +} function App(): React.JSX.Element { const isDarkMode = useColorScheme() === 'dark' - const [textContent, setTextContent] = useState('') - const [status, setStatus] = useState('Ready') + const [useSubstitution, setUseSubstitution] = useState(false) const theme = { - background: isDarkMode ? '#000000' : '#ffffff', + bg: isDarkMode ? '#000' : '#fff', surface: isDarkMode ? '#1c1c1e' : '#f2f2f7', - primary: isDarkMode ? '#007aff' : '#007aff', - secondary: isDarkMode ? '#34c759' : '#34c759', - text: isDarkMode ? '#ffffff' : '#000000', - textSecondary: isDarkMode ? '#8e8e93' : '#3c3c43', - border: isDarkMode ? '#38383a' : '#c6c6c8', - success: '#34c759', - error: '#ff3b30', - } - - const writeFile = async () => { - try { - setStatus('Writing file...') - await RNFS.writeFile(SHARED_FILE_PATH, textContent, 'utf8') - setStatus(`Successfully wrote: "${textContent}"`) - } catch (error) { - setStatus(`Write error: ${(error as Error).message}`) - } - } - - const readFile = async () => { - try { - setStatus('Reading file...') - const content = await RNFS.readFile(SHARED_FILE_PATH, 'utf8') - setTextContent(content) - setStatus(`Successfully read: "${content}"`) - } catch (error) { - setStatus(`Read error: ${(error as Error).message}`) - } - } - - const getStatusStyle = () => { - if (status.includes('error')) { - return {color: theme.error} - } - if (status.includes('Successfully')) { - return {color: theme.success} - } - return {color: theme.textSecondary} + text: isDarkMode ? '#fff' : '#000', + textSec: isDarkMode ? '#8e8e93' : '#6c6c70', + border: isDarkMode ? '#38383a' : '#d1d1d6', + blue: '#007aff', + green: '#34c759', + orange: '#ff9500', } return ( - + - - {/* Header */} - - - File System Sandbox Demo - - - Multi-instance file system access testing - - - - {/* Host Application Section */} + + {/* ===== HOST ===== */} + - - - Host Application - - - Primary - - - - - - - - Write File - - - - Read File - - - - - - Status: - - - {status} - - - - - {SHARED_FILE_PATH} + style={[styles.sectionHeader, {backgroundColor: theme.surface}]}> + + Host App + + HOST + - {/* Sandbox Sections */} + + + + {/* ===== SANDBOX ===== */} + - - - Sandbox: react-native-fs - - - Sandbox - + style={[styles.sectionHeader, {backgroundColor: theme.surface}]}> + + Sandbox + + + SANDBOXED - { - console.log('Host received message from sandbox:', message) - }} - onError={error => { - console.log('Host received error from sandbox:', error) - }} - /> - - - - Sandbox: react-native-file-access + + setUseSubstitution(v => !v)}> + + Module substitution{' '} + + {useSubstitution ? '(safe)' : '(off)'} + - - Sandbox - - - { - console.log('Host received message from sandbox:', message) - }} - onError={error => { - console.log('Host received error from sandbox:', error) - }} - /> + + + + console.log('Host received from sandbox:', msg)} + onError={err => + console.log('Host received error from sandbox:', err) + } + /> @@ -225,138 +134,51 @@ function App(): React.JSX.Element { } const styles = StyleSheet.create({ - container: { + root: { flex: 1, }, - header: { - paddingHorizontal: 20, - paddingVertical: 24, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.1, - shadowRadius: 4, - }, - android: { - elevation: 2, - }, - }), - }, - headerTitle: { - fontSize: 28, - fontWeight: '700', - letterSpacing: -0.5, - }, - headerSubtitle: { - fontSize: 16, - marginTop: 4, - fontWeight: '400', - }, - content: { - padding: 16, + section: { + borderBottomWidth: StyleSheet.hairlineWidth, }, - card: { - marginBottom: 20, - borderRadius: 12, - padding: 20, - borderWidth: 1, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 8, - }, - android: { - elevation: 3, - }, - }), - }, - cardHeader: { + sectionHeader: { flexDirection: 'row', - justifyContent: 'space-between', alignItems: 'center', - marginBottom: 16, + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 10, }, - cardTitle: { - fontSize: 18, - fontWeight: '600', - flex: 1, + sectionTitle: { + fontSize: 17, + fontWeight: '700', }, badge: { paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 6, + paddingVertical: 3, + borderRadius: 5, }, badgeText: { - color: '#ffffff', - fontSize: 12, - fontWeight: '600', - textTransform: 'uppercase', - }, - sandboxBadge: { - backgroundColor: '#ff6b35', + color: '#fff', + fontSize: 10, + fontWeight: '700', + letterSpacing: 0.5, }, - textInput: { - borderWidth: 1, - borderRadius: 8, - padding: 16, - marginBottom: 16, - minHeight: 100, - textAlignVertical: 'top', - fontSize: 16, - lineHeight: 22, + switchBar: { + paddingHorizontal: 16, + paddingVertical: 6, }, - buttonGroup: { + switchRow: { flexDirection: 'row', - gap: 12, - marginBottom: 16, - }, - button: { - flex: 1, - paddingVertical: 14, - paddingHorizontal: 20, - borderRadius: 8, alignItems: 'center', - justifyContent: 'center', - }, - primaryButton: { - // backgroundColor set dynamically - }, - secondaryButton: { - // backgroundColor set dynamically - }, - buttonText: { - color: '#ffffff', - fontWeight: '600', - fontSize: 16, - }, - statusContainer: { - padding: 12, - borderRadius: 8, - marginBottom: 12, + justifyContent: 'space-between', }, - statusLabel: { + switchLabel: { fontSize: 14, fontWeight: '500', - marginBottom: 4, - }, - statusText: { - fontSize: 14, - fontStyle: 'italic', - lineHeight: 20, - }, - pathText: { - fontSize: 12, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - opacity: 0.8, - lineHeight: 16, + flex: 1, }, - sandbox: { - height: 320, - borderWidth: 1, - borderRadius: 8, + sandboxView: { + height: 400, + marginBottom: 8, }, }) diff --git a/apps/fs-experiment/FileOpsUI.tsx b/apps/fs-experiment/FileOpsUI.tsx new file mode 100644 index 0000000..58020ea --- /dev/null +++ b/apps/fs-experiment/FileOpsUI.tsx @@ -0,0 +1,370 @@ +import AsyncStorage from '@react-native-async-storage/async-storage' +import React, {useEffect, useId, useState} from 'react' +import { + InputAccessoryView, + Keyboard, + Platform, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + useColorScheme, + View, +} from 'react-native' +import {Dirs, FileSystem} from 'react-native-file-access' + +// react-native-fs (v2) relies on the legacy NativeModules bridge. +// On Android NativeModules.RNFSManager is null under the New Architecture so +// RNFS is only available on iOS (where RN's bridge interop layer keeps it working). +// See https://github.com/itinance/react-native-fs/issues/1221 +let RNFS: any +let rnfsLoadError: string | null = null +if (Platform.OS === 'ios') { + try { + const mod = require('react-native-fs') + RNFS = mod.default ?? mod + } catch (e) { + rnfsLoadError = (e as Error).message + RNFS = {DocumentDirectoryPath: Dirs.DocumentDir} + } +} + +const MODULES = [ + ...(Platform.OS === 'ios' ? ([{key: 'rnfs', label: 'RNFS'}] as const) : []), + {key: 'file-access', label: 'file-access'}, + {key: 'async-storage', label: 'async-storage'}, +] as const +type Module = (typeof MODULES)[number]['key'] + +interface FileOpsUIProps { + accentColor?: string + initialModule?: Module + initialTarget?: string + initialContent?: string + onStatusChange?: (status: string) => void + testIDPrefix?: string +} + +export default function FileOpsUI({ + accentColor, + initialModule = 'file-access', + initialTarget = 'secret', + initialContent = '', + onStatusChange, + testIDPrefix = '', +}: FileOpsUIProps) { + const isDarkMode = useColorScheme() === 'dark' + const [module, setModule] = useState(initialModule) + const [target, setTarget] = useState(initialTarget) + const [text, setText] = useState(initialContent) + const [status, setStatus] = useState('Ready') + const accessoryId = useId() + const tid = (id: string) => (testIDPrefix ? `${testIDPrefix}-${id}` : id) + + useEffect(() => { + onStatusChange?.(status) + }, [status, onStatusChange]) + + const theme = { + bg: isDarkMode ? '#000' : '#fff', + surface: isDarkMode ? '#1c1c1e' : '#f2f2f7', + text: isDarkMode ? '#fff' : '#000', + textSec: isDarkMode ? '#8e8e93' : '#6c6c70', + border: isDarkMode ? '#38383a' : '#d1d1d6', + accent: accentColor ?? '#007aff', + green: '#34c759', + red: '#ff3b30', + segBg: isDarkMode ? '#2c2c2e' : '#e8e8ed', + segActive: isDarkMode ? '#3a3a3c' : '#fff', + } + + const isStorage = module === 'async-storage' + + const getPath = () => { + switch (module) { + case 'rnfs': + return `${RNFS?.DocumentDirectoryPath ?? Dirs.DocumentDir}/${target}` + case 'file-access': + return `${Dirs.DocumentDir}/${target}` + default: + return target + } + } + + const onWrite = async () => { + try { + if (module === 'rnfs' && rnfsLoadError) { + setStatus(`RNFS unavailable: ${rnfsLoadError}`) + return + } + setStatus('Writing...') + switch (module) { + case 'rnfs': + await RNFS.writeFile(getPath(), text, 'utf8') + break + case 'file-access': + await FileSystem.writeFile(getPath(), text) + break + case 'async-storage': + await AsyncStorage.setItem(target, text) + break + } + setStatus(`Wrote: "${text}"`) + } catch (e) { + setStatus(`Error: ${(e as Error).message}`) + } + } + + const onRead = async () => { + try { + if (module === 'rnfs' && rnfsLoadError) { + setStatus(`RNFS unavailable: ${rnfsLoadError}`) + return + } + setStatus('Reading...') + let content: string + switch (module) { + case 'rnfs': + content = await RNFS.readFile(getPath(), 'utf8') + break + case 'file-access': + content = await FileSystem.readFile(getPath()) + break + case 'async-storage': { + const val = await AsyncStorage.getItem(target) + content = val ?? '' + if (!val) { + setStatus(`Key "${target}" not found`) + return + } + break + } + } + setText(content) + setStatus(`Read: "${content}"`) + } catch (e) { + setStatus(`Error: ${(e as Error).message}`) + } + } + + const displayPath = isStorage ? `key: "${target}"` : `Documents/${target}` + + const statusColor = () => { + if (status.includes('BREACH') || status.includes('Error')) return theme.red + if (status.includes('Wrote') || status.includes('Read:')) return theme.green + return theme.textSec + } + + return ( + + + {MODULES.map(m => { + const active = m.key === module + return ( + setModule(m.key)}> + + {m.label} + + + ) + })} + + + + + + + + { + setText('') + setStatus('Ready') + }}> + Clear + + + Write + + + Read + + + + + {status} + + {displayPath} + + {Platform.OS === 'ios' && ( + + + + + Done + + + + + )} + + ) +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + padding: 12, + }, + segmented: { + flexDirection: 'row', + borderRadius: 8, + padding: 2, + marginBottom: 8, + }, + segItem: { + flex: 1, + paddingVertical: 6, + alignItems: 'center', + borderRadius: 6, + }, + segItemActive: { + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: {width: 0, height: 1}, + shadowOpacity: 0.12, + shadowRadius: 2, + }, + android: {elevation: 1}, + }), + }, + segText: { + fontSize: 11, + fontWeight: '500', + }, + segTextActive: { + fontWeight: '600', + }, + targetInput: { + borderWidth: 1, + borderRadius: 8, + paddingHorizontal: 10, + paddingVertical: 8, + fontSize: 13, + marginBottom: 6, + }, + contentInput: { + borderWidth: 1, + borderRadius: 8, + padding: 10, + minHeight: 64, + maxHeight: 80, + textAlignVertical: 'top', + fontSize: 14, + }, + buttonRow: { + flexDirection: 'row', + gap: 10, + marginVertical: 8, + }, + btn: { + flex: 1, + paddingVertical: 10, + borderRadius: 8, + alignItems: 'center', + }, + btnText: { + color: '#fff', + fontWeight: '600', + fontSize: 14, + }, + status: { + fontSize: 12, + fontStyle: 'italic', + }, + path: { + fontSize: 10, + fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', + marginTop: 2, + opacity: 0.7, + }, + accessory: { + flexDirection: 'row', + justifyContent: 'flex-end', + paddingHorizontal: 14, + paddingVertical: 8, + borderTopWidth: StyleSheet.hairlineWidth, + }, + accessoryBtn: { + fontSize: 16, + fontWeight: '600', + }, +}) diff --git a/apps/fs-experiment/README.md b/apps/fs-experiment/README.md index 4f842b2..22e72c0 100644 --- a/apps/fs-experiment/README.md +++ b/apps/fs-experiment/README.md @@ -1,14 +1,10 @@ -# File System Access Example +# File System & Storage Isolation ![Platform: iOS](https://img.shields.io/badge/platform-iOS-blue.svg) -This example demonstrates how to enable file system access in multi-instance environments by whitelisting the necessary native modules. The application shows how sandboxed React Native instances can be configured to access file system APIs when explicitly allowed. +This example demonstrates **TurboModule substitutions** β€” transparently replacing native module implementations inside a sandbox with scoped, per-origin alternatives. The app uses a split-screen layout where the host and sandbox run the same UI, but the sandbox can swap `react-native-fs` (iOS only), `react-native-file-access`, and `@react-native-async-storage/async-storage` for sandboxed implementations that jail file paths and scope storage per origin. -The experiment uses two popular React Native file system libraries: -- **react-native-fs** - Traditional file system operations -- **react-native-file-access** - Alternative file system API - -The host application creates multiple sandbox instances and demonstrates how to whitelist these modules to enable controlled file system access across instances while maintaining security boundaries. +> **Note:** `react-native-fs` (v2) relies on the legacy `NativeModules` bridge and does not support TurboModules / New Architecture. On **Android** `NativeModules.RNFSManager` is `null` so RNFS is **disabled** there. On **iOS** it still works thanks to RN's bridge interop layer. See [itinance/react-native-fs#1221](https://github.com/itinance/react-native-fs/issues/1221). ## Screenshot diff --git a/apps/fs-experiment/SandboxFS.tsx b/apps/fs-experiment/SandboxFS.tsx deleted file mode 100644 index 5d7704f..0000000 --- a/apps/fs-experiment/SandboxFS.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import React, {useState} from 'react' -import { - Platform, - SafeAreaView, - ScrollView, - StyleSheet, - Text, - TextInput, - TouchableOpacity, - useColorScheme, - View, -} from 'react-native' -// File system import -import RNFS from 'react-native-fs' - -const SHARED_FILE_PATH = `${RNFS.DocumentDirectoryPath}/shared_test_file.txt` - -function SandboxFS(): React.JSX.Element { - const isDarkMode = useColorScheme() === 'dark' - const [textContent, setTextContent] = useState('') - const [status, setStatus] = useState('Ready') - - const theme = { - background: isDarkMode ? '#000000' : '#ffffff', - surface: isDarkMode ? '#1c1c1e' : '#f2f2f7', - primary: isDarkMode ? '#ff6b35' : '#ff6b35', - secondary: isDarkMode ? '#34c759' : '#34c759', - text: isDarkMode ? '#ffffff' : '#000000', - textSecondary: isDarkMode ? '#8e8e93' : '#3c3c43', - border: isDarkMode ? '#38383a' : '#c6c6c8', - success: '#34c759', - error: '#ff3b30', - warning: '#ff9500', - } - - const writeFile = async () => { - try { - setStatus('Writing file...') - await RNFS.writeFile(SHARED_FILE_PATH, textContent, 'utf8') - setStatus(`Successfully wrote: "${textContent}"`) - } catch (error) { - setStatus(`Write error: ${(error as Error).message}`) - } - } - - const readFile = async () => { - try { - setStatus('Reading file...') - const content = await RNFS.readFile(SHARED_FILE_PATH, 'utf8') - setTextContent(content) - if (content.includes('Host')) { - setStatus(`SECURITY BREACH: Read host file: "${content}"`) - } else { - setStatus(`Successfully read: "${content}"`) - } - } catch (error) { - setStatus(`Read error: ${(error as Error).message}`) - } - } - - const getStatusStyle = () => { - if (status.includes('SECURITY BREACH')) { - return {color: theme.error, fontWeight: '600' as const} - } - if (status.includes('error')) { - return {color: theme.error} - } - if (status.includes('Successfully')) { - return {color: theme.success} - } - return {color: theme.textSecondary} - } - - return ( - - - {/* Header */} - - - - Sandbox Environment - - - RNFS - - - - React Native File System Implementation - - - - - - - File Operations - - - - - - - Write - - - - Read - - - - - - Operation Status: - - - {status} - - - - - - Target Path: - - - {SHARED_FILE_PATH} - - - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - header: { - paddingHorizontal: 16, - paddingVertical: 20, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.1, - shadowRadius: 4, - }, - android: { - elevation: 2, - }, - }), - }, - headerContent: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 4, - }, - title: { - fontSize: 20, - fontWeight: '700', - flex: 1, - }, - subtitle: { - fontSize: 14, - fontWeight: '400', - }, - badge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 6, - }, - badgeText: { - color: '#ffffff', - fontSize: 11, - fontWeight: '600', - textTransform: 'uppercase', - }, - content: { - padding: 16, - }, - card: { - borderRadius: 12, - padding: 20, - borderWidth: 1, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 8, - }, - android: { - elevation: 3, - }, - }), - }, - sectionTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 16, - }, - textInput: { - borderWidth: 1, - borderRadius: 8, - padding: 14, - marginBottom: 16, - minHeight: 80, - textAlignVertical: 'top', - fontSize: 15, - lineHeight: 20, - }, - buttonGroup: { - flexDirection: 'row', - gap: 10, - marginBottom: 16, - }, - button: { - flex: 1, - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - }, - buttonText: { - color: '#ffffff', - fontWeight: '600', - fontSize: 15, - }, - statusContainer: { - padding: 12, - borderRadius: 8, - marginBottom: 12, - }, - statusLabel: { - fontSize: 13, - fontWeight: '500', - marginBottom: 4, - }, - statusText: { - fontSize: 13, - fontStyle: 'italic', - lineHeight: 18, - }, - pathContainer: { - padding: 12, - borderRadius: 8, - }, - pathLabel: { - fontSize: 11, - fontWeight: '500', - marginBottom: 4, - textTransform: 'uppercase', - }, - pathText: { - fontSize: 10, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - opacity: 0.8, - lineHeight: 14, - }, -}) - -export default SandboxFS diff --git a/apps/fs-experiment/SandboxFileAccess.tsx b/apps/fs-experiment/SandboxFileAccess.tsx deleted file mode 100644 index 4a86a82..0000000 --- a/apps/fs-experiment/SandboxFileAccess.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import React, {useState} from 'react' -import { - Platform, - SafeAreaView, - ScrollView, - StyleSheet, - Text, - TextInput, - TouchableOpacity, - useColorScheme, - View, -} from 'react-native' -// File system import -import {Dirs, FileSystem} from 'react-native-file-access' - -const SHARED_FILE_PATH = `${Dirs.DocumentDir}/shared_test_file.txt` - -function SandboxFileAccess(): React.JSX.Element { - const isDarkMode = useColorScheme() === 'dark' - const [textContent, setTextContent] = useState('') - const [status, setStatus] = useState('Ready') - - const theme = { - background: isDarkMode ? '#000000' : '#ffffff', - surface: isDarkMode ? '#1c1c1e' : '#f2f2f7', - primary: isDarkMode ? '#9b59b6' : '#9b59b6', - secondary: isDarkMode ? '#34c759' : '#34c759', - text: isDarkMode ? '#ffffff' : '#000000', - textSecondary: isDarkMode ? '#8e8e93' : '#3c3c43', - border: isDarkMode ? '#38383a' : '#c6c6c8', - success: '#34c759', - error: '#ff3b30', - warning: '#ff9500', - } - - const writeFile = async () => { - try { - setStatus('Writing file...') - await FileSystem.writeFile(SHARED_FILE_PATH, textContent) - setStatus(`Successfully wrote: "${textContent}"`) - } catch (error) { - setStatus(`Write error: ${(error as Error).message}`) - } - } - - const readFile = async () => { - try { - setStatus('Reading file...') - const content = await FileSystem.readFile(SHARED_FILE_PATH) - setTextContent(content) - if (content.includes('Host')) { - setStatus(`SECURITY BREACH: Read host file: "${content}"`) - } else { - setStatus(`Successfully read: "${content}"`) - } - } catch (error) { - setStatus(`Read error: ${(error as Error).message}`) - } - } - - const getStatusStyle = () => { - if (status.includes('SECURITY BREACH')) { - return {color: theme.error, fontWeight: '600' as const} - } - if (status.includes('error')) { - return {color: theme.error} - } - if (status.includes('Successfully')) { - return {color: theme.success} - } - return {color: theme.textSecondary} - } - - return ( - - - {/* Header */} - - - - Sandbox Environment - - - File Access - - - - React Native File Access Implementation - - - - - - - File Operations - - - - - - - Write - - - - Read - - - - - - Operation Status: - - - {status} - - - - - - Target Path: - - - {SHARED_FILE_PATH} - - - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - header: { - paddingHorizontal: 16, - paddingVertical: 20, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.1, - shadowRadius: 4, - }, - android: { - elevation: 2, - }, - }), - }, - headerContent: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 4, - }, - title: { - fontSize: 20, - fontWeight: '700', - flex: 1, - }, - subtitle: { - fontSize: 14, - fontWeight: '400', - }, - badge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 6, - }, - badgeText: { - color: '#ffffff', - fontSize: 11, - fontWeight: '600', - textTransform: 'uppercase', - }, - content: { - padding: 16, - }, - card: { - borderRadius: 12, - padding: 20, - borderWidth: 1, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 8, - }, - android: { - elevation: 3, - }, - }), - }, - sectionTitle: { - fontSize: 16, - fontWeight: '600', - marginBottom: 16, - }, - textInput: { - borderWidth: 1, - borderRadius: 8, - padding: 14, - marginBottom: 16, - minHeight: 80, - textAlignVertical: 'top', - fontSize: 15, - lineHeight: 20, - }, - buttonGroup: { - flexDirection: 'row', - gap: 10, - marginBottom: 16, - }, - button: { - flex: 1, - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - }, - buttonText: { - color: '#ffffff', - fontWeight: '600', - fontSize: 15, - }, - statusContainer: { - padding: 12, - borderRadius: 8, - marginBottom: 12, - }, - statusLabel: { - fontSize: 13, - fontWeight: '500', - marginBottom: 4, - }, - statusText: { - fontSize: 13, - fontStyle: 'italic', - lineHeight: 18, - }, - pathContainer: { - padding: 12, - borderRadius: 8, - }, - pathLabel: { - fontSize: 11, - fontWeight: '500', - marginBottom: 4, - textTransform: 'uppercase', - }, - pathText: { - fontSize: 10, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - opacity: 0.8, - lineHeight: 14, - }, -}) - -export default SandboxFileAccess diff --git a/apps/fs-experiment/__tests__/sandbox-isolation.harness.tsx b/apps/fs-experiment/__tests__/sandbox-isolation.harness.tsx new file mode 100644 index 0000000..b39c19d --- /dev/null +++ b/apps/fs-experiment/__tests__/sandbox-isolation.harness.tsx @@ -0,0 +1,155 @@ +import {screen, userEvent} from '@react-native-harness/ui' +import React from 'react' +import {Platform} from 'react-native' +import { + afterAll, + beforeAll, + describe, + expect, + it, + render, +} from 'react-native-harness' + +import App from '../App' + +const isIOS = Platform.OS === 'ios' +const TEST_TIMEOUT = 60_000 +const RENDER_TIMEOUT = 10_000 + +async function sleep(ms: number) { + return new Promise(r => setTimeout(r, ms)) +} + +async function setSubstitution(enabled: boolean) { + const wantLabel = enabled ? 'substitution-on' : 'substitution-off' + if (screen.queryByAccessibilityLabel(wantLabel)) return + + await userEvent.press(await screen.findByTestId('substitution-switch')) + await screen.findByAccessibilityLabel(wantLabel) + await sleep(3000) +} + +async function writeAndRead( + mod: string, + writer: 'host' | 'sandbox', + reader: 'host' | 'sandbox', + substitution: boolean, + expectVisible: boolean +) { + const content = `${writer}_${mod}_${substitution ? 'on' : 'off'}` + + await setSubstitution(substitution) + + await userEvent.press(await screen.findByTestId(`${writer}-seg-${mod}`)) + await userEvent.press(await screen.findByTestId(`${writer}-clear-btn`)) + await userEvent.type( + await screen.findByTestId(`${writer}-content-input`), + content + ) + await userEvent.press(await screen.findByTestId(`${writer}-write-btn`)) + await screen.findByAccessibilityLabel(`Wrote: "${content}"`) + + await userEvent.press(await screen.findByTestId(`${reader}-seg-${mod}`)) + await userEvent.press(await screen.findByTestId(`${reader}-read-btn`)) + + if (expectVisible) { + await sleep(1000) + await screen.findByAccessibilityLabel(`Read: "${content}"`) + } else { + await sleep(2000) + const leaked = screen.queryByAccessibilityLabel(`Read: "${content}"`) + expect(leaked).toBeNull() + } +} + +// --------------------------------------------------------------------------- +// The harness tears down the rendered tree after every it() via an afterEach +// that calls store.getState().setRenderedElement(null). We intercept that at +// the store level so a single render() persists across all it() blocks. +// --------------------------------------------------------------------------- +const {store} = require('@react-native-harness/runtime/dist/ui/state.js') +const origSet = store.getState().setRenderedElement +let blockCleanup = false +store.getState().setRenderedElement = (el: unknown) => { + if (el === null && blockCleanup) return + origSet(el) +} + +// --------------------------------------------------------------------------- +// * rnfs β€” iOS only (react-native-fs incompatible with Android New Arch) +// * file-access OFF on Android β€” isolated due to SandboxContextWrapper +// * async-storage is not tested: because in memore data read only once at start +// --------------------------------------------------------------------------- + +describe('Substitution OFF', () => { + beforeAll(async () => { + blockCleanup = true + await render(, {timeout: RENDER_TIMEOUT}) + await sleep(4000) + }, 30_000) + + if (isIOS) { + it( + 'rnfs | sandbox -> host | shared', + () => writeAndRead('rnfs', 'sandbox', 'host', false, true), + TEST_TIMEOUT + ) + + it( + 'rnfs | host -> sandbox | shared', + () => writeAndRead('rnfs', 'host', 'sandbox', false, true), + TEST_TIMEOUT + ) + } + + it( + 'file-access | sandbox -> host | shared', + () => writeAndRead('file-access', 'sandbox', 'host', false, isIOS), + TEST_TIMEOUT + ) + + it( + 'file-access | host -> sandbox | shared', + () => writeAndRead('file-access', 'host', 'sandbox', false, isIOS), + TEST_TIMEOUT + ) +}) + +describe('Substitution ON', () => { + if (isIOS) { + it( + 'rnfs | sandbox -> host | isolated', + () => writeAndRead('rnfs', 'sandbox', 'host', true, false), + TEST_TIMEOUT + ) + + it( + 'rnfs | host -> sandbox | isolated', + () => writeAndRead('rnfs', 'host', 'sandbox', true, false), + TEST_TIMEOUT + ) + } + + // Android: toggling substitution remounts the SandboxReactNativeView but the + // old root's native nodes linger in the view hierarchy. The harness's + // queryByTestId returns stale nodes, making sandbox interaction unreliable. + if (isIOS) { + it( + 'file-access | sandbox -> host | isolated', + () => writeAndRead('file-access', 'sandbox', 'host', true, false), + TEST_TIMEOUT + ) + } + + it( + 'file-access | host -> sandbox | isolated', + () => writeAndRead('file-access', 'host', 'sandbox', true, false), + TEST_TIMEOUT + ) + + afterAll(() => { + blockCleanup = false + store.getState().setRenderedElement = origSet + origSet(null) + }) +}) diff --git a/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/MainApplication.kt b/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/MainApplication.kt index a2e0a6e..7f88ff0 100644 --- a/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/MainApplication.kt +++ b/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/MainApplication.kt @@ -11,6 +11,7 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader +import io.callstack.rnsandbox.SandboxReactNativeDelegate class MainApplication : Application(), ReactApplication { @@ -37,8 +38,9 @@ class MainApplication : Application(), ReactApplication { super.onCreate() SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. load() } + SandboxReactNativeDelegate.registerHostPackages(PackageList(this).packages) + SandboxReactNativeDelegate.registerSubstitutionPackages(SandboxedModulesPackage()) } } diff --git a/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedAsyncStorage.kt b/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedAsyncStorage.kt new file mode 100644 index 0000000..7685cd2 --- /dev/null +++ b/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedAsyncStorage.kt @@ -0,0 +1,332 @@ +package com.multinstance.fsexperiment + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.module.annotations.ReactModule +import io.callstack.rnsandbox.SandboxAwareModule +import org.json.JSONObject +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * Sandboxed AsyncStorage β€” per-origin SQLite storage that mirrors the original + * RNCAsyncStorage API but scopes data to the sandbox origin. + * + * Uses callbacks (not promises) to match the original AsyncStorageModule interface. + */ +@ReactModule(name = SandboxedAsyncStorage.MODULE_NAME) +class SandboxedAsyncStorage( + private val reactContext: ReactApplicationContext, +) : ReactContextBaseJavaModule(reactContext), SandboxAwareModule { + + companion object { + const val MODULE_NAME = "SandboxedAsyncStorage" + private const val TAG = "SandboxedAsyncStorage" + private const val TABLE = "kv" + private const val COL_KEY = "k" + private const val COL_VALUE = "v" + private const val DB_VERSION = 1 + private const val MAX_SQL_KEYS = 999 + } + + private val executor = Executors.newSingleThreadExecutor() + private var dbHelper: SandboxDBHelper? = null + @Volatile private var configured = false + + override fun getName(): String = MODULE_NAME + + override fun configureSandbox(origin: String, requestedName: String, resolvedName: String) { + Log.d(TAG, "Configuring for origin '$origin'") + val dbDir = java.io.File(reactContext.filesDir, "Sandboxes/$origin/AsyncStorage") + dbDir.mkdirs() + val dbName = "sandboxed_async_storage.db" + dbHelper = SandboxDBHelper(reactContext, java.io.File(dbDir, dbName).absolutePath) + configured = true + } + + override fun invalidate() { + executor.shutdown() + try { + executor.awaitTermination(2, TimeUnit.SECONDS) + } catch (_: InterruptedException) { + executor.shutdownNow() + } + dbHelper?.close() + dbHelper = null + configured = false + super.invalidate() + } + + private fun errorMap(message: String): WritableMap { + val map = Arguments.createMap() + map.putString("message", message) + return map + } + + private fun db(): SQLiteDatabase? = dbHelper?.writableDatabase + + private fun readDb(): SQLiteDatabase? = dbHelper?.readableDatabase + + @ReactMethod + fun multiGet(keys: ReadableArray, callback: Callback) { + if (!configured) { + callback.invoke(errorMap("Sandbox not configured"), null) + return + } + executor.execute { + try { + val db = readDb() ?: run { + callback.invoke(errorMap("Database not available"), null) + return@execute + } + val data = Arguments.createArray() + val keysRemaining = mutableSetOf() + + for (start in 0 until keys.size() step MAX_SQL_KEYS) { + val count = minOf(keys.size() - start, MAX_SQL_KEYS) + keysRemaining.clear() + val placeholders = (0 until count).joinToString(",") { "?" } + val args = Array(count) { keys.getString(start + it) ?: "" } + for (arg in args) keysRemaining.add(arg) + + db.rawQuery("SELECT $COL_KEY, $COL_VALUE FROM $TABLE WHERE $COL_KEY IN ($placeholders)", args).use { cursor -> + while (cursor.moveToNext()) { + val row = Arguments.createArray() + row.pushString(cursor.getString(0)) + row.pushString(cursor.getString(1)) + data.pushArray(row) + keysRemaining.remove(cursor.getString(0)) + } + } + + for (key in keysRemaining) { + val row = Arguments.createArray() + row.pushString(key) + row.pushNull() + data.pushArray(row) + } + } + callback.invoke(null, data) + } catch (e: Exception) { + Log.e(TAG, "multiGet failed", e) + callback.invoke(errorMap(e.message ?: "Unknown error"), null) + } + } + } + + @ReactMethod + fun multiSet(keyValueArray: ReadableArray, callback: Callback) { + if (!configured) { + callback.invoke(errorMap("Sandbox not configured")) + return + } + executor.execute { + try { + val db = db() ?: run { + callback.invoke(errorMap("Database not available")) + return@execute + } + db.beginTransaction() + try { + for (i in 0 until keyValueArray.size()) { + val pair = keyValueArray.getArray(i) ?: continue + if (pair.size() != 2) continue + val key = pair.getString(0) ?: continue + val value = pair.getString(1) ?: continue + + val cv = ContentValues() + cv.put(COL_KEY, key) + cv.put(COL_VALUE, value) + db.insertWithOnConflict(TABLE, null, cv, SQLiteDatabase.CONFLICT_REPLACE) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + callback.invoke() + } catch (e: Exception) { + Log.e(TAG, "multiSet failed", e) + callback.invoke(errorMap(e.message ?: "Unknown error")) + } + } + } + + @ReactMethod + fun multiRemove(keys: ReadableArray, callback: Callback) { + if (!configured) { + callback.invoke(errorMap("Sandbox not configured")) + return + } + executor.execute { + try { + val db = db() ?: run { + callback.invoke(errorMap("Database not available")) + return@execute + } + db.beginTransaction() + try { + for (start in 0 until keys.size() step MAX_SQL_KEYS) { + val count = minOf(keys.size() - start, MAX_SQL_KEYS) + val placeholders = (0 until count).joinToString(",") { "?" } + val args = Array(count) { keys.getString(start + it) ?: "" } + db.delete(TABLE, "$COL_KEY IN ($placeholders)", args) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + callback.invoke() + } catch (e: Exception) { + Log.e(TAG, "multiRemove failed", e) + callback.invoke(errorMap(e.message ?: "Unknown error")) + } + } + } + + @ReactMethod + fun multiMerge(keyValueArray: ReadableArray, callback: Callback) { + if (!configured) { + callback.invoke(errorMap("Sandbox not configured")) + return + } + executor.execute { + try { + val db = db() ?: run { + callback.invoke(errorMap("Database not available")) + return@execute + } + db.beginTransaction() + try { + for (i in 0 until keyValueArray.size()) { + val pair = keyValueArray.getArray(i) ?: continue + if (pair.size() != 2) continue + val key = pair.getString(0) ?: continue + val newValue = pair.getString(1) ?: continue + + val existing = getValueForKey(db, key) + val merged = if (existing != null) { + mergeJsonStrings(existing, newValue) ?: newValue + } else { + newValue + } + val cv = ContentValues() + cv.put(COL_KEY, key) + cv.put(COL_VALUE, merged) + db.insertWithOnConflict(TABLE, null, cv, SQLiteDatabase.CONFLICT_REPLACE) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + callback.invoke() + } catch (e: Exception) { + Log.e(TAG, "multiMerge failed", e) + callback.invoke(errorMap(e.message ?: "Unknown error")) + } + } + } + + @ReactMethod + fun getAllKeys(callback: Callback) { + if (!configured) { + callback.invoke(errorMap("Sandbox not configured"), null) + return + } + executor.execute { + try { + val db = readDb() ?: run { + callback.invoke(errorMap("Database not available"), null) + return@execute + } + val keys = Arguments.createArray() + db.rawQuery("SELECT $COL_KEY FROM $TABLE", null).use { cursor -> + while (cursor.moveToNext()) { + keys.pushString(cursor.getString(0)) + } + } + callback.invoke(null, keys) + } catch (e: Exception) { + Log.e(TAG, "getAllKeys failed", e) + callback.invoke(errorMap(e.message ?: "Unknown error"), null) + } + } + } + + @ReactMethod + fun clear(callback: Callback) { + if (!configured) { + callback.invoke(errorMap("Sandbox not configured")) + return + } + executor.execute { + try { + val db = db() ?: run { + callback.invoke(errorMap("Database not available")) + return@execute + } + db.delete(TABLE, null, null) + callback.invoke() + } catch (e: Exception) { + Log.e(TAG, "clear failed", e) + callback.invoke(errorMap(e.message ?: "Unknown error")) + } + } + } + + private fun getValueForKey(db: SQLiteDatabase, key: String): String? { + db.rawQuery("SELECT $COL_VALUE FROM $TABLE WHERE $COL_KEY = ?", arrayOf(key)).use { cursor -> + return if (cursor.moveToFirst()) cursor.getString(0) else null + } + } + + /** + * Deep recursive merge matching the original RNCAsyncStorage behavior: + * when both sides have a JSONObject at a given key, merge recursively; + * otherwise the new value overwrites the old one. + */ + private fun mergeJsonStrings(existing: String, incoming: String): String? { + return try { + val base = JSONObject(existing) + val overlay = JSONObject(incoming) + deepMerge(base, overlay) + base.toString() + } catch (e: Exception) { + null + } + } + + private fun deepMerge(base: JSONObject, overlay: JSONObject) { + for (key in overlay.keys()) { + val newValue = overlay.get(key) + val oldValue = base.opt(key) + if (oldValue is JSONObject && newValue is JSONObject) { + deepMerge(oldValue, newValue) + } else { + base.put(key, newValue) + } + } + } + + private class SandboxDBHelper(context: Context, dbPath: String) : + SQLiteOpenHelper(context, dbPath, null, DB_VERSION) { + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE ($COL_KEY TEXT PRIMARY KEY, $COL_VALUE TEXT NOT NULL)") + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("DROP TABLE IF EXISTS $TABLE") + onCreate(db) + } + } +} diff --git a/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedFileAccess.kt b/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedFileAccess.kt new file mode 100644 index 0000000..45539ec --- /dev/null +++ b/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedFileAccess.kt @@ -0,0 +1,423 @@ +package com.multinstance.fsexperiment + +import android.os.StatFs +import android.util.Base64 +import android.util.Log +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.module.annotations.ReactModule +import io.callstack.rnsandbox.SandboxAwareModule +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.RandomAccessFile +import java.security.MessageDigest +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * Sandboxed FileAccess β€” jails all file paths to a per-origin directory. + * + * Mirrors the iOS SandboxedFileAccess.mm implementation. Implements the + * react-native-file-access module interface so JS code works transparently. + */ +@ReactModule(name = SandboxedFileAccess.MODULE_NAME) +class SandboxedFileAccess( + private val reactContext: ReactApplicationContext, +) : ReactContextBaseJavaModule(reactContext), SandboxAwareModule { + + companion object { + const val MODULE_NAME = "SandboxedFileAccess" + private const val TAG = "SandboxedFileAccess" + } + + private val executor = Executors.newSingleThreadExecutor() + + private var sandboxRoot: String = "" + private var documentsDir: String = "" + private var cachesDir: String = "" + @Volatile private var configured = false + + override fun getName(): String = MODULE_NAME + + override fun configureSandbox(origin: String, requestedName: String, resolvedName: String) { + Log.d(TAG, "Configuring for origin '$origin'") + val base = File(reactContext.filesDir, "Sandboxes/$origin") + sandboxRoot = base.absolutePath + documentsDir = File(base, "Documents").absolutePath + cachesDir = File(base, "Caches").absolutePath + + listOf(documentsDir, cachesDir).forEach { File(it).mkdirs() } + configured = true + } + + override fun invalidate() { + executor.shutdown() + try { + executor.awaitTermination(2, TimeUnit.SECONDS) + } catch (_: InterruptedException) { + executor.shutdownNow() + } + configured = false + super.invalidate() + } + + private fun sandboxedPath(path: String, promise: Promise): String? { + if (!configured) { + promise.reject("EPERM", "SandboxedFileAccess: sandbox not configured.") + return null + } + val resolved = if (path.startsWith("/")) { + File(path).canonicalPath + } else { + File(documentsDir, path).canonicalPath + } + if (resolved.startsWith(sandboxRoot)) return resolved + promise.reject("EPERM", "Path '$path' is outside the sandbox. Allowed root: $sandboxRoot") + return null + } + + override fun getConstants(): Map { + if (!configured) return mapOf( + "CacheDir" to "", "DocumentDir" to "", "MainBundleDir" to "", + ) + return mapOf( + "CacheDir" to cachesDir, + "DocumentDir" to documentsDir, + "MainBundleDir" to documentsDir, + ) + } + + @ReactMethod + fun writeFile(path: String, data: String, encoding: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + File(safePath).parentFile?.mkdirs() + if (encoding == "base64") { + val decoded = Base64.decode(data, Base64.DEFAULT) + FileOutputStream(safePath).use { it.write(decoded) } + } else { + File(safePath).writeText(data, Charsets.UTF_8) + } + promise.resolve(null) + } catch (e: Exception) { + promise.reject("ERR", "Failed to write to '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun readFile(path: String, encoding: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + val file = File(safePath) + if (!file.exists()) { + promise.reject("ERR", "No such file '$path'") + return@execute + } + if (encoding == "base64") { + val bytes = file.readBytes() + promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP)) + } else { + promise.resolve(file.readText(Charsets.UTF_8)) + } + } catch (e: Exception) { + promise.reject("ERR", "Failed to read '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun readFileChunk(path: String, offset: Double, length: Double, encoding: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + RandomAccessFile(safePath, "r").use { raf -> + raf.seek(offset.toLong()) + val buf = ByteArray(length.toInt()) + val bytesRead = raf.read(buf) + if (bytesRead <= 0) { + promise.resolve("") + return@execute + } + val actual = buf.copyOf(bytesRead) + if (encoding == "base64") { + promise.resolve(Base64.encodeToString(actual, Base64.NO_WRAP)) + } else { + promise.resolve(String(actual, Charsets.UTF_8)) + } + } + } catch (e: Exception) { + promise.reject("ERR", "Failed to read chunk '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun appendFile(path: String, data: String, encoding: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + val bytes = if (encoding == "base64") { + Base64.decode(data, Base64.DEFAULT) + } else { + data.toByteArray(Charsets.UTF_8) + } + FileOutputStream(safePath, true).use { it.write(bytes) } + promise.resolve(null) + } catch (e: Exception) { + promise.reject("ERR", "Failed to append to '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun exists(path: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { promise.resolve(File(safePath).exists()) } + } + + @ReactMethod + fun isDir(path: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + val f = File(safePath) + promise.resolve(f.exists() && f.isDirectory) + } + } + + @ReactMethod + fun ls(path: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + val dir = File(safePath) + val result = Arguments.createArray() + dir.listFiles()?.forEach { result.pushString(it.name) } + promise.resolve(result) + } catch (e: Exception) { + promise.reject("ERR", "Failed to list '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun mkdir(path: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + File(safePath).mkdirs() + promise.resolve(safePath) + } catch (e: Exception) { + promise.reject("ERR", "Failed to mkdir '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun cp(source: String, target: String, promise: Promise) { + val src = sandboxedPath(source, promise) ?: return + val dst = sandboxedPath(target, promise) ?: return + executor.execute { + try { + File(dst).parentFile?.mkdirs() + File(src).copyTo(File(dst), overwrite = true) + promise.resolve(null) + } catch (e: Exception) { + promise.reject("ERR", "Failed to copy '$source' to '$target': ${e.message}", e) + } + } + } + + @ReactMethod + fun mv(source: String, target: String, promise: Promise) { + val src = sandboxedPath(source, promise) ?: return + val dst = sandboxedPath(target, promise) ?: return + executor.execute { + try { + File(dst).parentFile?.mkdirs() + val srcFile = File(src) + val dstFile = File(dst) + dstFile.delete() + if (!srcFile.renameTo(dstFile)) { + srcFile.copyTo(dstFile, overwrite = true) + srcFile.delete() + } + promise.resolve(null) + } catch (e: Exception) { + promise.reject("ERR", "Failed to move '$source' to '$target': ${e.message}", e) + } + } + } + + @ReactMethod + fun unlink(path: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + val file = File(safePath) + if (file.isDirectory) file.deleteRecursively() else file.delete() + promise.resolve(null) + } catch (e: Exception) { + promise.reject("ERR", "Failed to unlink '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun stat(path: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + val file = File(safePath) + if (!file.exists()) { + promise.reject("ERR", "No such file '$path'") + return@execute + } + val result = Arguments.createMap() + result.putString("filename", file.name) + result.putDouble("lastModified", file.lastModified().toDouble()) + result.putString("path", safePath) + result.putDouble("size", file.length().toDouble()) + result.putString("type", if (file.isDirectory) "directory" else "file") + promise.resolve(result) + } catch (e: Exception) { + promise.reject("ERR", "Failed to stat '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun statDir(path: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + val dir = File(safePath) + val results = Arguments.createArray() + dir.listFiles()?.forEach { file -> + val item = Arguments.createMap() + item.putString("filename", file.name) + item.putDouble("lastModified", file.lastModified().toDouble()) + item.putString("path", file.absolutePath) + item.putDouble("size", file.length().toDouble()) + item.putString("type", if (file.isDirectory) "directory" else "file") + results.pushMap(item) + } + promise.resolve(results) + } catch (e: Exception) { + promise.reject("ERR", "Failed to statDir '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun hash(path: String, algorithm: String, promise: Promise) { + val safePath = sandboxedPath(path, promise) ?: return + executor.execute { + try { + val algoMap = mapOf( + "MD5" to "MD5", "SHA-1" to "SHA-1", "SHA-256" to "SHA-256", "SHA-512" to "SHA-512" + ) + val javaAlgo = algoMap[algorithm] + if (javaAlgo == null) { + promise.reject("ERR", "Unknown algorithm '$algorithm'") + return@execute + } + val md = MessageDigest.getInstance(javaAlgo) + FileInputStream(safePath).use { fis -> + val buf = ByteArray(8192) + var len: Int + while (fis.read(buf).also { len = it } != -1) md.update(buf, 0, len) + } + promise.resolve(md.digest().joinToString("") { "%02x".format(it) }) + } catch (e: Exception) { + promise.reject("ERR", "Failed to hash '$path': ${e.message}", e) + } + } + } + + @ReactMethod + fun concatFiles(source: String, target: String, promise: Promise) { + val src = sandboxedPath(source, promise) ?: return + val dst = sandboxedPath(target, promise) ?: return + executor.execute { + try { + var totalBytes = 0L + FileInputStream(src).use { input -> + FileOutputStream(dst, true).use { output -> + val buf = ByteArray(8192) + var len: Int + while (input.read(buf).also { len = it } != -1) { + output.write(buf, 0, len) + totalBytes += len + } + } + } + promise.resolve(totalBytes.toDouble()) + } catch (e: Exception) { + promise.reject("ERR", "Failed to concat '$source' to '$target': ${e.message}", e) + } + } + } + + @ReactMethod + fun df(promise: Promise) { + if (!configured) { + promise.reject("EPERM", "SandboxedFileAccess: sandbox not configured.") + return + } + executor.execute { + try { + val stat = StatFs(sandboxRoot) + val result = Arguments.createMap() + result.putDouble("internal_free", stat.availableBytes.toDouble()) + result.putDouble("internal_total", stat.totalBytes.toDouble()) + promise.resolve(result) + } catch (e: Exception) { + promise.reject("ERR", "Failed to stat filesystem: ${e.message}", e) + } + } + } + + @ReactMethod + fun fetch(requestId: Double, resource: String, init: ReadableMap) { + Log.w(TAG, "fetch is not available in sandboxed mode") + } + + @ReactMethod + fun cancelFetch(requestId: Double, promise: Promise) { + promise.resolve(null) + } + + @ReactMethod + fun cpAsset(asset: String, target: String, type: String, promise: Promise) { + promise.reject("EPERM", "cpAsset is not available in sandboxed mode") + } + + @ReactMethod + fun cpExternal(source: String, targetName: String, dir: String, promise: Promise) { + promise.reject("EPERM", "cpExternal is not available in sandboxed mode") + } + + @ReactMethod + fun getAppGroupDir(groupName: String, promise: Promise) { + promise.reject("EPERM", "getAppGroupDir is not available in sandboxed mode") + } + + @ReactMethod + fun unzip(source: String, target: String, promise: Promise) { + promise.reject("EPERM", "unzip is not available in sandboxed mode") + } + + @ReactMethod + fun addListener(eventName: String) = Unit + + @ReactMethod + fun removeListeners(count: Double) = Unit +} diff --git a/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedModulesPackage.kt b/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedModulesPackage.kt new file mode 100644 index 0000000..a9e600f --- /dev/null +++ b/apps/fs-experiment/android/app/src/main/java/com/multinstance/fsexperiment/SandboxedModulesPackage.kt @@ -0,0 +1,46 @@ +package com.multinstance.fsexperiment + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider +import com.facebook.react.uimanager.ViewManager + +/** + * ReactPackage providing sandboxed module implementations for the fs-experiment app. + * + * Registered via SandboxReactNativeDelegate.registerSubstitutionPackages() in + * MainApplication.onCreate() so the sandbox's FilteredReactPackage can resolve + * substitution targets. + */ +class SandboxedModulesPackage : BaseReactPackage() { + + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return when (name) { + SandboxedFileAccess.MODULE_NAME -> SandboxedFileAccess(reactContext) + SandboxedAsyncStorage.MODULE_NAME -> SandboxedAsyncStorage(reactContext) + else -> null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + mapOf( + SandboxedFileAccess.MODULE_NAME to ReactModuleInfo( + SandboxedFileAccess.MODULE_NAME, + SandboxedFileAccess.MODULE_NAME, + false, false, false, false, false, + ), + SandboxedAsyncStorage.MODULE_NAME to ReactModuleInfo( + SandboxedAsyncStorage.MODULE_NAME, + SandboxedAsyncStorage.MODULE_NAME, + false, false, false, false, false, + ), + ) + } + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> = + emptyList() +} diff --git a/apps/fs-experiment/docs/screenshot.png b/apps/fs-experiment/docs/screenshot.png index c777c82..e33eb42 100644 Binary files a/apps/fs-experiment/docs/screenshot.png and b/apps/fs-experiment/docs/screenshot.png differ diff --git a/apps/fs-experiment/index.js b/apps/fs-experiment/index.js index aa27c82..f5279ed 100644 --- a/apps/fs-experiment/index.js +++ b/apps/fs-experiment/index.js @@ -4,4 +4,3 @@ import App from './App' import {name as appName} from './app.json' AppRegistry.registerComponent(appName, () => App) -AppRegistry.registerComponent('App', () => App) // Register App for recursion diff --git a/apps/fs-experiment/ios/AppDelegate.swift b/apps/fs-experiment/ios/AppDelegate.swift index 831c037..6749b40 100644 --- a/apps/fs-experiment/ios/AppDelegate.swift +++ b/apps/fs-experiment/ios/AppDelegate.swift @@ -20,7 +20,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let viewController = UIViewController() viewController.view = reactNativeFactory.rootViewFactory.view( - withModuleName: "App", + withModuleName: "MultInstance-FSExperiment", initialProperties: [:], launchOptions: launchOptions ) @@ -42,7 +42,7 @@ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { #if DEBUG RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") #else - Bundle.main.url(forResource: jsBundleName, withExtension: "jsbundle") + Bundle.main.url(forResource: "main", withExtension: "jsbundle") #endif } } diff --git a/apps/fs-experiment/ios/MultInstance-FSExperiment.xcodeproj/project.pbxproj b/apps/fs-experiment/ios/MultInstance-FSExperiment.xcodeproj/project.pbxproj index f0e2763..67ba2b1 100644 --- a/apps/fs-experiment/ios/MultInstance-FSExperiment.xcodeproj/project.pbxproj +++ b/apps/fs-experiment/ios/MultInstance-FSExperiment.xcodeproj/project.pbxproj @@ -8,10 +8,13 @@ /* Begin PBXBuildFile section */ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 16A9D245FE0364DDA6A37BB5 /* SandboxedRNCAsyncStorage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73D74517051F649BF3AF385E /* SandboxedRNCAsyncStorage.mm */; }; 43DD6316E596C4F3419573F4 /* libPods-MultInstance-FSExperiment.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BD4A3E0B4C109F8DD0FAE9D /* libPods-MultInstance-FSExperiment.a */; }; 575209B0052EDA94007D9B65 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + A1B2C3D4E5F60001DEADBEEF /* SandboxedRNFSManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60002DEADBEEF /* SandboxedRNFSManager.mm */; }; + A1B2C3D4E5F60003DEADBEEF /* SandboxedFileAccess.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60004DEADBEEF /* SandboxedFileAccess.mm */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -23,8 +26,14 @@ 4144D1AD685F86583FAB67C6 /* Pods-MultInstance-FSExperiment.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultInstance-FSExperiment.release.xcconfig"; path = "Target Support Files/Pods-MultInstance-FSExperiment/Pods-MultInstance-FSExperiment.release.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-MultInstance-FSExperiment.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultInstance-FSExperiment.release.xcconfig"; path = "Target Support Files/Pods-MultInstance-FSExperiment/Pods-MultInstance-FSExperiment.release.xcconfig"; sourceTree = ""; }; 6BD4A3E0B4C109F8DD0FAE9D /* libPods-MultInstance-FSExperiment.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MultInstance-FSExperiment.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 73D74517051F649BF3AF385E /* SandboxedRNCAsyncStorage.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = SandboxedRNCAsyncStorage.mm; sourceTree = ""; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = "MultInstance-FSExperiment/LaunchScreen.storyboard"; sourceTree = ""; }; + A1B2C3D4E5F60002DEADBEEF /* SandboxedRNFSManager.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = SandboxedRNFSManager.mm; sourceTree = ""; }; + A1B2C3D4E5F60004DEADBEEF /* SandboxedFileAccess.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = SandboxedFileAccess.mm; sourceTree = ""; }; + A1B2C3D4E5F60005DEADBEEF /* SandboxedRNFSManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SandboxedRNFSManager.h; sourceTree = ""; }; + A1B2C3D4E5F60006DEADBEEF /* SandboxedFileAccess.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SandboxedFileAccess.h; sourceTree = ""; }; + B8F0D39C18571332952E64A6 /* SandboxedRNCAsyncStorage.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SandboxedRNCAsyncStorage.h; sourceTree = ""; }; E4C18DFA4C4A23A5EFB8579E /* Pods-MultInstance-FSExperiment.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultInstance-FSExperiment.debug.xcconfig"; path = "Target Support Files/Pods-MultInstance-FSExperiment/Pods-MultInstance-FSExperiment.debug.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -49,6 +58,12 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, + B8F0D39C18571332952E64A6 /* SandboxedRNCAsyncStorage.h */, + 73D74517051F649BF3AF385E /* SandboxedRNCAsyncStorage.mm */, + A1B2C3D4E5F60005DEADBEEF /* SandboxedRNFSManager.h */, + A1B2C3D4E5F60002DEADBEEF /* SandboxedRNFSManager.mm */, + A1B2C3D4E5F60006DEADBEEF /* SandboxedFileAccess.h */, + A1B2C3D4E5F60004DEADBEEF /* SandboxedFileAccess.mm */, ); name = "MultInstance-FSExperiment"; sourceTree = ""; @@ -251,6 +266,9 @@ buildActionMask = 2147483647; files = ( 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */, + 16A9D245FE0364DDA6A37BB5 /* SandboxedRNCAsyncStorage.mm in Sources */, + A1B2C3D4E5F60001DEADBEEF /* SandboxedRNFSManager.mm in Sources */, + A1B2C3D4E5F60003DEADBEEF /* SandboxedFileAccess.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -392,7 +410,7 @@ "$(inherited)", " ", ); - REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; USE_HERMES = true; @@ -470,7 +488,7 @@ "$(inherited)", " ", ); - REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; VALIDATE_PRODUCT = YES; diff --git a/apps/fs-experiment/ios/Podfile.lock b/apps/fs-experiment/ios/Podfile.lock index 607299b..0f7c58b 100644 --- a/apps/fs-experiment/ios/Podfile.lock +++ b/apps/fs-experiment/ios/Podfile.lock @@ -2,12 +2,40 @@ PODS: - boost (1.84.0) - DoubleConversion (1.1.6) - fast_float (8.0.0) - - FBLazyVector (0.80.1) + - FBLazyVector (0.82.1) - fmt (11.0.2) - glog (0.3.5) - - hermes-engine (0.80.1): - - hermes-engine/Pre-built (= 0.80.1) - - hermes-engine/Pre-built (0.80.1) + - HarnessUI (1.0.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - hermes-engine (0.82.1): + - hermes-engine/Pre-built (= 0.82.1) + - hermes-engine/Pre-built (0.82.1) - RCT-Folly (2024.11.18.00): - boost - DoubleConversion @@ -27,27 +55,27 @@ PODS: - fast_float (= 8.0.0) - fmt (= 11.0.2) - glog - - RCTDeprecation (0.80.1) - - RCTRequired (0.80.1) - - RCTTypeSafety (0.80.1): - - FBLazyVector (= 0.80.1) - - RCTRequired (= 0.80.1) - - React-Core (= 0.80.1) - - React (0.80.1): - - React-Core (= 0.80.1) - - React-Core/DevSupport (= 0.80.1) - - React-Core/RCTWebSocket (= 0.80.1) - - React-RCTActionSheet (= 0.80.1) - - React-RCTAnimation (= 0.80.1) - - React-RCTBlob (= 0.80.1) - - React-RCTImage (= 0.80.1) - - React-RCTLinking (= 0.80.1) - - React-RCTNetwork (= 0.80.1) - - React-RCTSettings (= 0.80.1) - - React-RCTText (= 0.80.1) - - React-RCTVibration (= 0.80.1) - - React-callinvoker (0.80.1) - - React-Core (0.80.1): + - RCTDeprecation (0.82.1) + - RCTRequired (0.82.1) + - RCTTypeSafety (0.82.1): + - FBLazyVector (= 0.82.1) + - RCTRequired (= 0.82.1) + - React-Core (= 0.82.1) + - React (0.82.1): + - React-Core (= 0.82.1) + - React-Core/DevSupport (= 0.82.1) + - React-Core/RCTWebSocket (= 0.82.1) + - React-RCTActionSheet (= 0.82.1) + - React-RCTAnimation (= 0.82.1) + - React-RCTBlob (= 0.82.1) + - React-RCTImage (= 0.82.1) + - React-RCTLinking (= 0.82.1) + - React-RCTNetwork (= 0.82.1) + - React-RCTSettings (= 0.82.1) + - React-RCTText (= 0.82.1) + - React-RCTVibration (= 0.82.1) + - React-callinvoker (0.82.1) + - React-Core (0.82.1): - boost - DoubleConversion - fast_float @@ -57,7 +85,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.80.1) + - React-Core/Default (= 0.82.1) - React-cxxreact - React-featureflags - React-hermes @@ -67,11 +95,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/CoreModulesHeaders (0.80.1): + - React-Core/CoreModulesHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -91,11 +120,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/Default (0.80.1): + - React-Core/Default (0.82.1): - boost - DoubleConversion - fast_float @@ -114,11 +144,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/DevSupport (0.80.1): + - React-Core/DevSupport (0.82.1): - boost - DoubleConversion - fast_float @@ -128,8 +159,8 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.80.1) - - React-Core/RCTWebSocket (= 0.80.1) + - React-Core/Default (= 0.82.1) + - React-Core/RCTWebSocket (= 0.82.1) - React-cxxreact - React-featureflags - React-hermes @@ -139,11 +170,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTActionSheetHeaders (0.80.1): + - React-Core/RCTActionSheetHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -163,11 +195,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTAnimationHeaders (0.80.1): + - React-Core/RCTAnimationHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -187,11 +220,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTBlobHeaders (0.80.1): + - React-Core/RCTBlobHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -211,11 +245,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTImageHeaders (0.80.1): + - React-Core/RCTImageHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -235,11 +270,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTLinkingHeaders (0.80.1): + - React-Core/RCTLinkingHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -259,11 +295,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTNetworkHeaders (0.80.1): + - React-Core/RCTNetworkHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -283,11 +320,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTSettingsHeaders (0.80.1): + - React-Core/RCTSettingsHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -307,11 +345,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTTextHeaders (0.80.1): + - React-Core/RCTTextHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -331,11 +370,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTVibrationHeaders (0.80.1): + - React-Core/RCTVibrationHeaders (0.82.1): - boost - DoubleConversion - fast_float @@ -355,11 +395,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-Core/RCTWebSocket (0.80.1): + - React-Core/RCTWebSocket (0.82.1): - boost - DoubleConversion - fast_float @@ -369,7 +410,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.80.1) + - React-Core/Default (= 0.82.1) - React-cxxreact - React-featureflags - React-hermes @@ -379,11 +420,12 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-CoreModules (0.80.1): + - React-CoreModules (0.82.1): - boost - DoubleConversion - fast_float @@ -391,19 +433,21 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - RCTTypeSafety (= 0.80.1) - - React-Core/CoreModulesHeaders (= 0.80.1) - - React-jsi (= 0.80.1) + - RCTTypeSafety (= 0.82.1) + - React-Core/CoreModulesHeaders (= 0.82.1) + - React-debug + - React-jsi (= 0.82.1) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - React-NativeModulesApple - React-RCTBlob - React-RCTFBReactNativeSpec - - React-RCTImage (= 0.80.1) + - React-RCTImage (= 0.82.1) + - React-runtimeexecutor - ReactCommon - SocketRocket - - React-cxxreact (0.80.1): + - React-cxxreact (0.82.1): - boost - DoubleConversion - fast_float @@ -412,19 +456,19 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.80.1) - - React-debug (= 0.80.1) - - React-jsi (= 0.80.1) + - React-callinvoker (= 0.82.1) + - React-debug (= 0.82.1) + - React-jsi (= 0.82.1) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - - React-logger (= 0.80.1) - - React-perflogger (= 0.80.1) - - React-runtimeexecutor (= 0.80.1) - - React-timing (= 0.80.1) + - React-logger (= 0.82.1) + - React-perflogger (= 0.82.1) + - React-runtimeexecutor + - React-timing (= 0.82.1) - SocketRocket - - React-debug (0.80.1) - - React-defaultsnativemodule (0.80.1): + - React-debug (0.82.1) + - React-defaultsnativemodule (0.82.1): - boost - DoubleConversion - fast_float @@ -435,14 +479,14 @@ PODS: - RCT-Folly/Fabric - React-domnativemodule - React-featureflagsnativemodule - - React-hermes - React-idlecallbacksnativemodule - React-jsi - React-jsiexecutor - React-microtasksnativemodule - React-RCTFBReactNativeSpec + - React-webperformancenativemodule - SocketRocket - - React-domnativemodule (0.80.1): + - React-domnativemodule (0.82.1): - boost - DoubleConversion - fast_float @@ -452,16 +496,17 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - React-Fabric + - React-Fabric/bridging - React-FabricComponents - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec + - React-runtimeexecutor - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-Fabric (0.80.1): + - React-Fabric (0.82.1): - boost - DoubleConversion - fast_float @@ -475,34 +520,35 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/animations (= 0.80.1) - - React-Fabric/attributedstring (= 0.80.1) - - React-Fabric/componentregistry (= 0.80.1) - - React-Fabric/componentregistrynative (= 0.80.1) - - React-Fabric/components (= 0.80.1) - - React-Fabric/consistency (= 0.80.1) - - React-Fabric/core (= 0.80.1) - - React-Fabric/dom (= 0.80.1) - - React-Fabric/imagemanager (= 0.80.1) - - React-Fabric/leakchecker (= 0.80.1) - - React-Fabric/mounting (= 0.80.1) - - React-Fabric/observers (= 0.80.1) - - React-Fabric/scheduler (= 0.80.1) - - React-Fabric/telemetry (= 0.80.1) - - React-Fabric/templateprocessor (= 0.80.1) - - React-Fabric/uimanager (= 0.80.1) + - React-Fabric/animations (= 0.82.1) + - React-Fabric/attributedstring (= 0.82.1) + - React-Fabric/bridging (= 0.82.1) + - React-Fabric/componentregistry (= 0.82.1) + - React-Fabric/componentregistrynative (= 0.82.1) + - React-Fabric/components (= 0.82.1) + - React-Fabric/consistency (= 0.82.1) + - React-Fabric/core (= 0.82.1) + - React-Fabric/dom (= 0.82.1) + - React-Fabric/imagemanager (= 0.82.1) + - React-Fabric/leakchecker (= 0.82.1) + - React-Fabric/mounting (= 0.82.1) + - React-Fabric/observers (= 0.82.1) + - React-Fabric/scheduler (= 0.82.1) + - React-Fabric/telemetry (= 0.82.1) + - React-Fabric/templateprocessor (= 0.82.1) + - React-Fabric/uimanager (= 0.82.1) - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/animations (0.80.1): + - React-Fabric/animations (0.82.1): - boost - DoubleConversion - fast_float @@ -518,16 +564,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/attributedstring (0.80.1): + - React-Fabric/attributedstring (0.82.1): - boost - DoubleConversion - fast_float @@ -543,16 +589,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/componentregistry (0.80.1): + - React-Fabric/bridging (0.82.1): - boost - DoubleConversion - fast_float @@ -568,16 +614,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/componentregistrynative (0.80.1): + - React-Fabric/componentregistry (0.82.1): - boost - DoubleConversion - fast_float @@ -593,16 +639,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components (0.80.1): + - React-Fabric/componentregistrynative (0.82.1): - boost - DoubleConversion - fast_float @@ -616,22 +662,18 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/components/legacyviewmanagerinterop (= 0.80.1) - - React-Fabric/components/root (= 0.80.1) - - React-Fabric/components/scrollview (= 0.80.1) - - React-Fabric/components/view (= 0.80.1) - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/legacyviewmanagerinterop (0.80.1): + - React-Fabric/components (0.82.1): - boost - DoubleConversion - fast_float @@ -645,18 +687,22 @@ PODS: - React-Core - React-cxxreact - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.82.1) + - React-Fabric/components/root (= 0.82.1) + - React-Fabric/components/scrollview (= 0.82.1) + - React-Fabric/components/view (= 0.82.1) - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/root (0.80.1): + - React-Fabric/components/legacyviewmanagerinterop (0.82.1): - boost - DoubleConversion - fast_float @@ -672,16 +718,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/scrollview (0.80.1): + - React-Fabric/components/root (0.82.1): - boost - DoubleConversion - fast_float @@ -697,16 +743,41 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/view (0.80.1): + - React-Fabric/components/scrollview (0.82.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - SocketRocket + - React-Fabric/components/view (0.82.1): - boost - DoubleConversion - fast_float @@ -722,18 +793,18 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-renderercss - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-Fabric/consistency (0.80.1): + - React-Fabric/consistency (0.82.1): - boost - DoubleConversion - fast_float @@ -749,16 +820,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/core (0.80.1): + - React-Fabric/core (0.82.1): - boost - DoubleConversion - fast_float @@ -774,16 +845,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/dom (0.80.1): + - React-Fabric/dom (0.82.1): - boost - DoubleConversion - fast_float @@ -799,16 +870,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/imagemanager (0.80.1): + - React-Fabric/imagemanager (0.82.1): - boost - DoubleConversion - fast_float @@ -824,16 +895,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/leakchecker (0.80.1): + - React-Fabric/leakchecker (0.82.1): - boost - DoubleConversion - fast_float @@ -849,16 +920,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/mounting (0.80.1): + - React-Fabric/mounting (0.82.1): - boost - DoubleConversion - fast_float @@ -874,16 +945,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/observers (0.80.1): + - React-Fabric/observers (0.82.1): - boost - DoubleConversion - fast_float @@ -897,19 +968,19 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/observers/events (= 0.80.1) + - React-Fabric/observers/events (= 0.82.1) - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/observers/events (0.80.1): + - React-Fabric/observers/events (0.82.1): - boost - DoubleConversion - fast_float @@ -925,16 +996,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/scheduler (0.80.1): + - React-Fabric/scheduler (0.82.1): - boost - DoubleConversion - fast_float @@ -951,17 +1022,18 @@ PODS: - React-Fabric/observers/events - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-performancecdpmetrics - React-performancetimeline - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/telemetry (0.80.1): + - React-Fabric/telemetry (0.82.1): - boost - DoubleConversion - fast_float @@ -977,16 +1049,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/templateprocessor (0.80.1): + - React-Fabric/templateprocessor (0.82.1): - boost - DoubleConversion - fast_float @@ -1002,16 +1074,16 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/uimanager (0.80.1): + - React-Fabric/uimanager (0.82.1): - boost - DoubleConversion - fast_float @@ -1025,20 +1097,20 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/uimanager/consistency (= 0.80.1) + - React-Fabric/uimanager/consistency (= 0.82.1) - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererconsistency - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/uimanager/consistency (0.80.1): + - React-Fabric/uimanager/consistency (0.82.1): - boost - DoubleConversion - fast_float @@ -1054,17 +1126,17 @@ PODS: - React-debug - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger - React-rendererconsistency - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-FabricComponents (0.80.1): + - React-FabricComponents (0.82.1): - boost - DoubleConversion - fast_float @@ -1079,21 +1151,21 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components (= 0.80.1) - - React-FabricComponents/textlayoutmanager (= 0.80.1) + - React-FabricComponents/components (= 0.82.1) + - React-FabricComponents/textlayoutmanager (= 0.82.1) - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components (0.80.1): + - React-FabricComponents/components (0.82.1): - boost - DoubleConversion - fast_float @@ -1108,28 +1180,31 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components/inputaccessory (= 0.80.1) - - React-FabricComponents/components/iostextinput (= 0.80.1) - - React-FabricComponents/components/modal (= 0.80.1) - - React-FabricComponents/components/rncore (= 0.80.1) - - React-FabricComponents/components/safeareaview (= 0.80.1) - - React-FabricComponents/components/scrollview (= 0.80.1) - - React-FabricComponents/components/text (= 0.80.1) - - React-FabricComponents/components/textinput (= 0.80.1) - - React-FabricComponents/components/unimplementedview (= 0.80.1) + - React-FabricComponents/components/inputaccessory (= 0.82.1) + - React-FabricComponents/components/iostextinput (= 0.82.1) + - React-FabricComponents/components/modal (= 0.82.1) + - React-FabricComponents/components/rncore (= 0.82.1) + - React-FabricComponents/components/safeareaview (= 0.82.1) + - React-FabricComponents/components/scrollview (= 0.82.1) + - React-FabricComponents/components/switch (= 0.82.1) + - React-FabricComponents/components/text (= 0.82.1) + - React-FabricComponents/components/textinput (= 0.82.1) + - React-FabricComponents/components/unimplementedview (= 0.82.1) + - React-FabricComponents/components/virtualview (= 0.82.1) + - React-FabricComponents/components/virtualviewexperimental (= 0.82.1) - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/inputaccessory (0.80.1): + - React-FabricComponents/components/inputaccessory (0.82.1): - boost - DoubleConversion - fast_float @@ -1146,17 +1221,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/iostextinput (0.80.1): + - React-FabricComponents/components/iostextinput (0.82.1): - boost - DoubleConversion - fast_float @@ -1173,17 +1248,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/modal (0.80.1): + - React-FabricComponents/components/modal (0.82.1): - boost - DoubleConversion - fast_float @@ -1200,17 +1275,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/rncore (0.80.1): + - React-FabricComponents/components/rncore (0.82.1): - boost - DoubleConversion - fast_float @@ -1227,17 +1302,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/safeareaview (0.80.1): + - React-FabricComponents/components/safeareaview (0.82.1): - boost - DoubleConversion - fast_float @@ -1254,17 +1329,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/scrollview (0.80.1): + - React-FabricComponents/components/scrollview (0.82.1): - boost - DoubleConversion - fast_float @@ -1281,17 +1356,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/text (0.80.1): + - React-FabricComponents/components/switch (0.82.1): - boost - DoubleConversion - fast_float @@ -1308,17 +1383,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/textinput (0.80.1): + - React-FabricComponents/components/text (0.82.1): - boost - DoubleConversion - fast_float @@ -1335,17 +1410,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/unimplementedview (0.80.1): + - React-FabricComponents/components/textinput (0.82.1): - boost - DoubleConversion - fast_float @@ -1362,17 +1437,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/textlayoutmanager (0.80.1): + - React-FabricComponents/components/unimplementedview (0.82.1): - boost - DoubleConversion - fast_float @@ -1389,17 +1464,17 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-RCTFBReactNativeSpec - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricImage (0.80.1): + - React-FabricComponents/components/virtualview (0.82.1): - boost - DoubleConversion - fast_float @@ -1408,22 +1483,102 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - RCTRequired (= 0.80.1) - - RCTTypeSafety (= 0.80.1) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - React-FabricComponents/components/virtualviewexperimental (0.82.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - React-FabricComponents/textlayoutmanager (0.82.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - React-FabricImage (0.82.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired (= 0.82.1) + - RCTTypeSafety (= 0.82.1) - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-ImageManager - React-jsi - - React-jsiexecutor (= 0.80.1) + - React-jsiexecutor (= 0.82.1) - React-logger - React-rendererdebug - React-utils - ReactCommon - SocketRocket - Yoga - - React-featureflags (0.80.1): + - React-featureflags (0.82.1): - boost - DoubleConversion - fast_float @@ -1432,7 +1587,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-featureflagsnativemodule (0.80.1): + - React-featureflagsnativemodule (0.82.1): - boost - DoubleConversion - fast_float @@ -1442,13 +1597,12 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - React-featureflags - - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - React-graphics (0.80.1): + - React-graphics (0.82.1): - boost - DoubleConversion - fast_float @@ -1457,12 +1611,11 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-hermes - React-jsi - React-jsiexecutor - React-utils - SocketRocket - - React-hermes (0.80.1): + - React-hermes (0.82.1): - boost - DoubleConversion - fast_float @@ -1471,16 +1624,17 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-cxxreact (= 0.80.1) + - React-cxxreact (= 0.82.1) - React-jsi - - React-jsiexecutor (= 0.80.1) + - React-jsiexecutor (= 0.82.1) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - - React-perflogger (= 0.80.1) + - React-oscompat + - React-perflogger (= 0.82.1) - React-runtimeexecutor - SocketRocket - - React-idlecallbacksnativemodule (0.80.1): + - React-idlecallbacksnativemodule (0.82.1): - boost - DoubleConversion - fast_float @@ -1489,14 +1643,14 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec + - React-runtimeexecutor - React-runtimescheduler - ReactCommon/turbomodule/core - SocketRocket - - React-ImageManager (0.80.1): + - React-ImageManager (0.82.1): - boost - DoubleConversion - fast_float @@ -1511,7 +1665,7 @@ PODS: - React-rendererdebug - React-utils - SocketRocket - - React-jserrorhandler (0.80.1): + - React-jserrorhandler (0.82.1): - boost - DoubleConversion - fast_float @@ -1526,7 +1680,7 @@ PODS: - React-jsi - ReactCommon/turbomodule/bridging - SocketRocket - - React-jsi (0.80.1): + - React-jsi (0.82.1): - boost - DoubleConversion - fast_float @@ -1536,7 +1690,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-jsiexecutor (0.80.1): + - React-jsiexecutor (0.82.1): - boost - DoubleConversion - fast_float @@ -1545,14 +1699,16 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-cxxreact (= 0.80.1) - - React-jsi (= 0.80.1) + - React-cxxreact + - React-debug + - React-jsi - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - - React-perflogger (= 0.80.1) + - React-perflogger + - React-runtimeexecutor - SocketRocket - - React-jsinspector (0.80.1): + - React-jsinspector (0.82.1): - boost - DoubleConversion - fast_float @@ -1566,10 +1722,11 @@ PODS: - React-jsinspectorcdp - React-jsinspectornetwork - React-jsinspectortracing - - React-perflogger (= 0.80.1) - - React-runtimeexecutor (= 0.80.1) + - React-oscompat + - React-perflogger (= 0.82.1) + - React-runtimeexecutor - SocketRocket - - React-jsinspectorcdp (0.80.1): + - React-jsinspectorcdp (0.82.1): - boost - DoubleConversion - fast_float @@ -1578,7 +1735,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-jsinspectornetwork (0.80.1): + - React-jsinspectornetwork (0.82.1): - boost - DoubleConversion - fast_float @@ -1586,9 +1743,12 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric + - React-featureflags - React-jsinspectorcdp + - React-performancetimeline + - React-timing - SocketRocket - - React-jsinspectortracing (0.80.1): + - React-jsinspectortracing (0.82.1): - boost - DoubleConversion - fast_float @@ -1597,8 +1757,9 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - React-oscompat + - React-timing - SocketRocket - - React-jsitooling (0.80.1): + - React-jsitooling (0.82.1): - boost - DoubleConversion - fast_float @@ -1606,15 +1767,17 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - React-cxxreact (= 0.80.1) - - React-jsi (= 0.80.1) + - React-cxxreact (= 0.82.1) + - React-debug + - React-jsi (= 0.82.1) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing + - React-runtimeexecutor - SocketRocket - - React-jsitracing (0.80.1): + - React-jsitracing (0.82.1): - React-jsi - - React-logger (0.80.1): + - React-logger (0.82.1): - boost - DoubleConversion - fast_float @@ -1623,7 +1786,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-Mapbuffer (0.80.1): + - React-Mapbuffer (0.82.1): - boost - DoubleConversion - fast_float @@ -1633,7 +1796,7 @@ PODS: - RCT-Folly/Fabric - React-debug - SocketRocket - - React-microtasksnativemodule (0.80.1): + - React-microtasksnativemodule (0.82.1): - boost - DoubleConversion - fast_float @@ -1642,13 +1805,12 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - React-NativeModulesApple (0.80.1): + - React-NativeModulesApple (0.82.1): - boost - DoubleConversion - fast_float @@ -1660,8 +1822,8 @@ PODS: - React-callinvoker - React-Core - React-cxxreact + - React-debug - React-featureflags - - React-hermes - React-jsi - React-jsinspector - React-jsinspectorcdp @@ -1669,8 +1831,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - React-oscompat (0.80.1) - - React-perflogger (0.80.1): + - React-oscompat (0.82.1) + - React-perflogger (0.82.1): - boost - DoubleConversion - fast_float @@ -1679,7 +1841,21 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-performancetimeline (0.80.1): + - React-performancecdpmetrics (0.82.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - React-jsi + - React-performancetimeline + - React-runtimeexecutor + - React-timing + - SocketRocket + - React-performancetimeline (0.82.1): - boost - DoubleConversion - fast_float @@ -1692,9 +1868,9 @@ PODS: - React-perflogger - React-timing - SocketRocket - - React-RCTActionSheet (0.80.1): - - React-Core/RCTActionSheetHeaders (= 0.80.1) - - React-RCTAnimation (0.80.1): + - React-RCTActionSheet (0.82.1): + - React-Core/RCTActionSheetHeaders (= 0.82.1) + - React-RCTAnimation (0.82.1): - boost - DoubleConversion - fast_float @@ -1710,7 +1886,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTAppDelegate (0.80.1): + - React-RCTAppDelegate (0.82.1): - boost - DoubleConversion - fast_float @@ -1739,11 +1915,12 @@ PODS: - React-rendererdebug - React-RuntimeApple - React-RuntimeCore + - React-runtimeexecutor - React-runtimescheduler - React-utils - ReactCommon - SocketRocket - - React-RCTBlob (0.80.1): + - React-RCTBlob (0.82.1): - boost - DoubleConversion - fast_float @@ -1762,7 +1939,7 @@ PODS: - React-RCTNetwork - ReactCommon - SocketRocket - - React-RCTFabric (0.80.1): + - React-RCTFabric (0.82.1): - boost - DoubleConversion - fast_float @@ -1778,25 +1955,27 @@ PODS: - React-FabricImage - React-featureflags - React-graphics - - React-hermes - React-ImageManager - React-jsi - React-jsinspector - React-jsinspectorcdp - React-jsinspectornetwork - React-jsinspectortracing + - React-performancecdpmetrics - React-performancetimeline - React-RCTAnimation + - React-RCTFBReactNativeSpec - React-RCTImage - React-RCTText - React-rendererconsistency - React-renderercss - React-rendererdebug + - React-runtimeexecutor - React-runtimescheduler - React-utils - SocketRocket - Yoga - - React-RCTFBReactNativeSpec (0.80.1): + - React-RCTFBReactNativeSpec (0.82.1): - boost - DoubleConversion - fast_float @@ -1808,13 +1987,35 @@ PODS: - RCTRequired - RCTTypeSafety - React-Core - - React-hermes - React-jsi - - React-jsiexecutor - React-NativeModulesApple + - React-RCTFBReactNativeSpec/components (= 0.82.1) - ReactCommon - SocketRocket - - React-RCTImage (0.80.1): + - React-RCTFBReactNativeSpec/components (0.82.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon + - SocketRocket + - Yoga + - React-RCTImage (0.82.1): - boost - DoubleConversion - fast_float @@ -1830,14 +2031,14 @@ PODS: - React-RCTNetwork - ReactCommon - SocketRocket - - React-RCTLinking (0.80.1): - - React-Core/RCTLinkingHeaders (= 0.80.1) - - React-jsi (= 0.80.1) + - React-RCTLinking (0.82.1): + - React-Core/RCTLinkingHeaders (= 0.82.1) + - React-jsi (= 0.82.1) - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - ReactCommon/turbomodule/core (= 0.80.1) - - React-RCTNetwork (0.80.1): + - ReactCommon/turbomodule/core (= 0.82.1) + - React-RCTNetwork (0.82.1): - boost - DoubleConversion - fast_float @@ -1847,6 +2048,7 @@ PODS: - RCT-Folly/Fabric - RCTTypeSafety - React-Core/RCTNetworkHeaders + - React-debug - React-featureflags - React-jsi - React-jsinspectorcdp @@ -1855,7 +2057,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTRuntime (0.80.1): + - React-RCTRuntime (0.82.1): - boost - DoubleConversion - fast_float @@ -1865,7 +2067,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - React-Core - - React-hermes + - React-debug - React-jsi - React-jsinspector - React-jsinspectorcdp @@ -1873,9 +2075,10 @@ PODS: - React-jsitooling - React-RuntimeApple - React-RuntimeCore + - React-runtimeexecutor - React-RuntimeHermes - SocketRocket - - React-RCTSettings (0.80.1): + - React-RCTSettings (0.82.1): - boost - DoubleConversion - fast_float @@ -1890,10 +2093,10 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTText (0.80.1): - - React-Core/RCTTextHeaders (= 0.80.1) + - React-RCTText (0.82.1): + - React-Core/RCTTextHeaders (= 0.82.1) - Yoga - - React-RCTVibration (0.80.1): + - React-RCTVibration (0.82.1): - boost - DoubleConversion - fast_float @@ -1907,11 +2110,11 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-rendererconsistency (0.80.1) - - React-renderercss (0.80.1): + - React-rendererconsistency (0.82.1) + - React-renderercss (0.82.1): - React-debug - React-utils - - React-rendererdebug (0.80.1): + - React-rendererdebug (0.82.1): - boost - DoubleConversion - fast_float @@ -1921,8 +2124,7 @@ PODS: - RCT-Folly/Fabric - React-debug - SocketRocket - - React-rncore (0.80.1) - - React-RuntimeApple (0.80.1): + - React-RuntimeApple (0.82.1): - boost - DoubleConversion - fast_float @@ -1951,7 +2153,7 @@ PODS: - React-runtimescheduler - React-utils - SocketRocket - - React-RuntimeCore (0.80.1): + - React-RuntimeCore (0.82.1): - boost - DoubleConversion - fast_float @@ -1963,7 +2165,6 @@ PODS: - React-cxxreact - React-Fabric - React-featureflags - - React-hermes - React-jserrorhandler - React-jsi - React-jsiexecutor @@ -1974,9 +2175,20 @@ PODS: - React-runtimescheduler - React-utils - SocketRocket - - React-runtimeexecutor (0.80.1): - - React-jsi (= 0.80.1) - - React-RuntimeHermes (0.80.1): + - React-runtimeexecutor (0.82.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric + - React-debug + - React-featureflags + - React-jsi (= 0.82.1) + - React-utils + - SocketRocket + - React-RuntimeHermes (0.82.1): - boost - DoubleConversion - fast_float @@ -1994,9 +2206,10 @@ PODS: - React-jsitooling - React-jsitracing - React-RuntimeCore + - React-runtimeexecutor - React-utils - SocketRocket - - React-runtimescheduler (0.80.1): + - React-runtimescheduler (0.82.1): - boost - DoubleConversion - fast_float @@ -2009,7 +2222,6 @@ PODS: - React-cxxreact - React-debug - React-featureflags - - React-hermes - React-jsi - React-jsinspectortracing - React-performancetimeline @@ -2019,7 +2231,7 @@ PODS: - React-timing - React-utils - SocketRocket - - React-Sandbox (0.4.0): + - React-Sandbox (0.5.0): - boost - DoubleConversion - fast_float @@ -2035,7 +2247,6 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-ImageManager - React-jsi - React-NativeModulesApple @@ -2048,8 +2259,9 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-timing (0.80.1) - - React-utils (0.80.1): + - React-timing (0.82.1): + - React-debug + - React-utils (0.82.1): - boost - DoubleConversion - fast_float @@ -2059,12 +2271,27 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - React-debug - - React-hermes - - React-jsi (= 0.80.1) + - React-jsi (= 0.82.1) + - SocketRocket + - React-webperformancenativemodule (0.82.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - React-jsi + - React-jsiexecutor + - React-performancetimeline + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core - SocketRocket - - ReactAppDependencyProvider (0.80.1): + - ReactAppDependencyProvider (0.82.1): - ReactCodegen - - ReactCodegen (0.80.1): + - ReactCodegen (0.82.1): - boost - DoubleConversion - fast_float @@ -2081,7 +2308,6 @@ PODS: - React-FabricImage - React-featureflags - React-graphics - - React-hermes - React-jsi - React-jsiexecutor - React-NativeModulesApple @@ -2091,7 +2317,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - ReactCommon (0.80.1): + - ReactCommon (0.82.1): - boost - DoubleConversion - fast_float @@ -2099,9 +2325,9 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - ReactCommon/turbomodule (= 0.80.1) + - ReactCommon/turbomodule (= 0.82.1) - SocketRocket - - ReactCommon/turbomodule (0.80.1): + - ReactCommon/turbomodule (0.82.1): - boost - DoubleConversion - fast_float @@ -2110,15 +2336,15 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.80.1) - - React-cxxreact (= 0.80.1) - - React-jsi (= 0.80.1) - - React-logger (= 0.80.1) - - React-perflogger (= 0.80.1) - - ReactCommon/turbomodule/bridging (= 0.80.1) - - ReactCommon/turbomodule/core (= 0.80.1) + - React-callinvoker (= 0.82.1) + - React-cxxreact (= 0.82.1) + - React-jsi (= 0.82.1) + - React-logger (= 0.82.1) + - React-perflogger (= 0.82.1) + - ReactCommon/turbomodule/bridging (= 0.82.1) + - ReactCommon/turbomodule/core (= 0.82.1) - SocketRocket - - ReactCommon/turbomodule/bridging (0.80.1): + - ReactCommon/turbomodule/bridging (0.82.1): - boost - DoubleConversion - fast_float @@ -2127,13 +2353,13 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.80.1) - - React-cxxreact (= 0.80.1) - - React-jsi (= 0.80.1) - - React-logger (= 0.80.1) - - React-perflogger (= 0.80.1) + - React-callinvoker (= 0.82.1) + - React-cxxreact (= 0.82.1) + - React-jsi (= 0.82.1) + - React-logger (= 0.82.1) + - React-perflogger (= 0.82.1) - SocketRocket - - ReactCommon/turbomodule/core (0.80.1): + - ReactCommon/turbomodule/core (0.82.1): - boost - DoubleConversion - fast_float @@ -2142,14 +2368,14 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.80.1) - - React-cxxreact (= 0.80.1) - - React-debug (= 0.80.1) - - React-featureflags (= 0.80.1) - - React-jsi (= 0.80.1) - - React-logger (= 0.80.1) - - React-perflogger (= 0.80.1) - - React-utils (= 0.80.1) + - React-callinvoker (= 0.82.1) + - React-cxxreact (= 0.82.1) + - React-debug (= 0.82.1) + - React-featureflags (= 0.82.1) + - React-jsi (= 0.82.1) + - React-logger (= 0.82.1) + - React-perflogger (= 0.82.1) + - React-utils (= 0.82.1) - SocketRocket - ReactNativeFileAccess (3.1.1): - boost @@ -2167,7 +2393,6 @@ PODS: - React-Fabric - React-featureflags - React-graphics - - React-hermes - React-ImageManager - React-jsi - React-NativeModulesApple @@ -2181,6 +2406,34 @@ PODS: - SocketRocket - Yoga - ZIPFoundation + - RNCAsyncStorage (2.2.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - RNFS (2.20.0): - React-Core - SocketRocket (0.7.1) @@ -2188,83 +2441,86 @@ PODS: - ZIPFoundation (0.9.19) DEPENDENCIES: - - boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - - fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) - - fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`) - - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) - - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - - RCTRequired (from `../../../node_modules/react-native/Libraries/Required`) - - RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`) - - React (from `../../../node_modules/react-native/`) - - React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`) - - React-Core (from `../../../node_modules/react-native/`) - - React-Core/RCTWebSocket (from `../../../node_modules/react-native/`) - - React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`) - - React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`) - - React-debug (from `../../../node_modules/react-native/ReactCommon/react/debug`) - - React-defaultsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) - - React-domnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/dom`) - - React-Fabric (from `../../../node_modules/react-native/ReactCommon`) - - React-FabricComponents (from `../../../node_modules/react-native/ReactCommon`) - - React-FabricImage (from `../../../node_modules/react-native/ReactCommon`) - - React-featureflags (from `../../../node_modules/react-native/ReactCommon/react/featureflags`) - - React-featureflagsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) - - React-graphics (from `../../../node_modules/react-native/ReactCommon/react/renderer/graphics`) - - React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`) - - React-idlecallbacksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) - - React-ImageManager (from `../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) - - React-jserrorhandler (from `../../../node_modules/react-native/ReactCommon/jserrorhandler`) - - React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`) - - React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`) - - React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`) - - React-jsinspectorcdp (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) - - React-jsinspectornetwork (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/network`) - - React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) - - React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`) - - React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`) - - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) - - React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`) - - React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - - React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - - React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`) - - React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`) - - React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`) - - React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`) - - React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`) - - React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`) - - React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`) - - React-RCTFabric (from `../../../node_modules/react-native/React`) - - React-RCTFBReactNativeSpec (from `../../../node_modules/react-native/React`) - - React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`) - - React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`) - - React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`) - - React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`) - - React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`) - - React-RCTText (from `../../../node_modules/react-native/Libraries/Text`) - - React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`) - - React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`) - - React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`) - - React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`) - - React-rncore (from `../../../node_modules/react-native/ReactCommon`) - - React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) - - React-RuntimeCore (from `../../../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`) - - React-RuntimeHermes (from `../../../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimescheduler (from `../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - "HarnessUI (from `../../../node_modules/@react-native-harness/ui`)" + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../node_modules/react-native/Libraries/Required`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) + - React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`) + - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancecdpmetrics (from `../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) + - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - "React-Sandbox (from `../../../node_modules/@callstack/react-native-sandbox`)" - - React-timing (from `../../../node_modules/react-native/ReactCommon/react/timing`) - - React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`) + - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - React-webperformancenativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - ReactNativeFileAccess (from `../../../node_modules/react-native-file-access`) + - "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)" - RNFS (from `../../../node_modules/react-native-fs`) - SocketRocket (~> 0.7.1) - - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: @@ -2273,236 +2529,245 @@ SPEC REPOS: EXTERNAL SOURCES: boost: - :podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec" + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DoubleConversion: - :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: - :podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec" + :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: - :path: "../../../node_modules/react-native/Libraries/FBLazyVector" + :path: "../node_modules/react-native/Libraries/FBLazyVector" fmt: - :podspec: "../../../node_modules/react-native/third-party-podspecs/fmt.podspec" + :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" glog: - :podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec" + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + HarnessUI: + :path: "../../../node_modules/@react-native-harness/ui" hermes-engine: - :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" - :tag: hermes-2025-05-06-RNv0.80.0-4eb6132a5bf0450bf4c6c91987675381d7ac8bca + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-2025-09-01-RNv0.82.0-265ef62ff3eb7289d17e366664ac0da82303e101 RCT-Folly: - :podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: - :path: "../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" RCTRequired: - :path: "../../../node_modules/react-native/Libraries/Required" + :path: "../node_modules/react-native/Libraries/Required" RCTTypeSafety: - :path: "../../../node_modules/react-native/Libraries/TypeSafety" + :path: "../node_modules/react-native/Libraries/TypeSafety" React: - :path: "../../../node_modules/react-native/" + :path: "../node_modules/react-native/" React-callinvoker: - :path: "../../../node_modules/react-native/ReactCommon/callinvoker" + :path: "../node_modules/react-native/ReactCommon/callinvoker" React-Core: - :path: "../../../node_modules/react-native/" + :path: "../node_modules/react-native/" React-CoreModules: - :path: "../../../node_modules/react-native/React/CoreModules" + :path: "../node_modules/react-native/React/CoreModules" React-cxxreact: - :path: "../../../node_modules/react-native/ReactCommon/cxxreact" + :path: "../node_modules/react-native/ReactCommon/cxxreact" React-debug: - :path: "../../../node_modules/react-native/ReactCommon/react/debug" + :path: "../node_modules/react-native/ReactCommon/react/debug" React-defaultsnativemodule: - :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" React-domnativemodule: - :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/dom" + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" React-Fabric: - :path: "../../../node_modules/react-native/ReactCommon" + :path: "../node_modules/react-native/ReactCommon" React-FabricComponents: - :path: "../../../node_modules/react-native/ReactCommon" + :path: "../node_modules/react-native/ReactCommon" React-FabricImage: - :path: "../../../node_modules/react-native/ReactCommon" + :path: "../node_modules/react-native/ReactCommon" React-featureflags: - :path: "../../../node_modules/react-native/ReactCommon/react/featureflags" + :path: "../node_modules/react-native/ReactCommon/react/featureflags" React-featureflagsnativemodule: - :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" React-graphics: - :path: "../../../node_modules/react-native/ReactCommon/react/renderer/graphics" + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" React-hermes: - :path: "../../../node_modules/react-native/ReactCommon/hermes" + :path: "../node_modules/react-native/ReactCommon/hermes" React-idlecallbacksnativemodule: - :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" React-ImageManager: - :path: "../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" React-jserrorhandler: - :path: "../../../node_modules/react-native/ReactCommon/jserrorhandler" + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" React-jsi: - :path: "../../../node_modules/react-native/ReactCommon/jsi" + :path: "../node_modules/react-native/ReactCommon/jsi" React-jsiexecutor: - :path: "../../../node_modules/react-native/ReactCommon/jsiexecutor" + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: - :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern" + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" React-jsinspectorcdp: - :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" React-jsinspectornetwork: - :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/network" + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network" React-jsinspectortracing: - :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" React-jsitooling: - :path: "../../../node_modules/react-native/ReactCommon/jsitooling" + :path: "../node_modules/react-native/ReactCommon/jsitooling" React-jsitracing: - :path: "../../../node_modules/react-native/ReactCommon/hermes/executor/" + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" React-logger: - :path: "../../../node_modules/react-native/ReactCommon/logger" + :path: "../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: - :path: "../../../node_modules/react-native/ReactCommon" + :path: "../node_modules/react-native/ReactCommon" React-microtasksnativemodule: - :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" React-NativeModulesApple: - :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-oscompat: - :path: "../../../node_modules/react-native/ReactCommon/oscompat" + :path: "../node_modules/react-native/ReactCommon/oscompat" React-perflogger: - :path: "../../../node_modules/react-native/ReactCommon/reactperflogger" + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-performancecdpmetrics: + :path: "../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" React-performancetimeline: - :path: "../../../node_modules/react-native/ReactCommon/react/performance/timeline" + :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" React-RCTActionSheet: - :path: "../../../node_modules/react-native/Libraries/ActionSheetIOS" + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" React-RCTAnimation: - :path: "../../../node_modules/react-native/Libraries/NativeAnimation" + :path: "../node_modules/react-native/Libraries/NativeAnimation" React-RCTAppDelegate: - :path: "../../../node_modules/react-native/Libraries/AppDelegate" + :path: "../node_modules/react-native/Libraries/AppDelegate" React-RCTBlob: - :path: "../../../node_modules/react-native/Libraries/Blob" + :path: "../node_modules/react-native/Libraries/Blob" React-RCTFabric: - :path: "../../../node_modules/react-native/React" + :path: "../node_modules/react-native/React" React-RCTFBReactNativeSpec: - :path: "../../../node_modules/react-native/React" + :path: "../node_modules/react-native/React" React-RCTImage: - :path: "../../../node_modules/react-native/Libraries/Image" + :path: "../node_modules/react-native/Libraries/Image" React-RCTLinking: - :path: "../../../node_modules/react-native/Libraries/LinkingIOS" + :path: "../node_modules/react-native/Libraries/LinkingIOS" React-RCTNetwork: - :path: "../../../node_modules/react-native/Libraries/Network" + :path: "../node_modules/react-native/Libraries/Network" React-RCTRuntime: - :path: "../../../node_modules/react-native/React/Runtime" + :path: "../node_modules/react-native/React/Runtime" React-RCTSettings: - :path: "../../../node_modules/react-native/Libraries/Settings" + :path: "../node_modules/react-native/Libraries/Settings" React-RCTText: - :path: "../../../node_modules/react-native/Libraries/Text" + :path: "../node_modules/react-native/Libraries/Text" React-RCTVibration: - :path: "../../../node_modules/react-native/Libraries/Vibration" + :path: "../node_modules/react-native/Libraries/Vibration" React-rendererconsistency: - :path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency" + :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" React-renderercss: - :path: "../../../node_modules/react-native/ReactCommon/react/renderer/css" + :path: "../node_modules/react-native/ReactCommon/react/renderer/css" React-rendererdebug: - :path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug" - React-rncore: - :path: "../../../node_modules/react-native/ReactCommon" + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" React-RuntimeApple: - :path: "../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" React-RuntimeCore: - :path: "../../../node_modules/react-native/ReactCommon/react/runtime" + :path: "../node_modules/react-native/ReactCommon/react/runtime" React-runtimeexecutor: - :path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor" + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" React-RuntimeHermes: - :path: "../../../node_modules/react-native/ReactCommon/react/runtime" + :path: "../node_modules/react-native/ReactCommon/react/runtime" React-runtimescheduler: - :path: "../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" React-Sandbox: :path: "../../../node_modules/@callstack/react-native-sandbox" React-timing: - :path: "../../../node_modules/react-native/ReactCommon/react/timing" + :path: "../node_modules/react-native/ReactCommon/react/timing" React-utils: - :path: "../../../node_modules/react-native/ReactCommon/react/utils" + :path: "../node_modules/react-native/ReactCommon/react/utils" + React-webperformancenativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" ReactAppDependencyProvider: :path: build/generated/ios ReactCodegen: :path: build/generated/ios ReactCommon: - :path: "../../../node_modules/react-native/ReactCommon" + :path: "../node_modules/react-native/ReactCommon" ReactNativeFileAccess: :path: "../../../node_modules/react-native-file-access" + RNCAsyncStorage: + :path: "../../../node_modules/@react-native-async-storage/async-storage" RNFS: :path: "../../../node_modules/react-native-fs" Yoga: - :path: "../../../node_modules/react-native/ReactCommon/yoga" + :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 - FBLazyVector: 09f03e4b6f42f955734b64a118f86509cc719427 + FBLazyVector: 0aa6183b9afe3c31fc65b5d1eeef1f3c19b63bfa fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 4f07404533b808de66cf48ac4200463068d0e95a + HarnessUI: 23b272c7d3a0a3628479d1287c1d4bd59b562636 + hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 - RCTDeprecation: efa5010912100e944a7ac9a93a157e1def1988fe - RCTRequired: bbc4cf999ddc4a4b076e076c74dd1d39d0254630 - RCTTypeSafety: d877728097547d0a37786cc9130c43ad71739ac3 - React: 4b0b9cb962e694611e5e8a697c1b0300a2510c21 - React-callinvoker: 70f125c17c7132811a6b473946ac5e7ae93b5e57 - React-Core: bab40f5b1f46fe0c5896895a6f333e861a821a81 - React-CoreModules: 05647d952e521113c128360633896ba7ba652e82 - React-cxxreact: 2b4bac1ec6eecc6288ac8a6caea6afb42585740e - React-debug: deb3a146ef717fa3e8f4c23e0288369fe53199b7 - React-defaultsnativemodule: 11e2948787a15d3cf1b66d7f29f13770a177bff7 - React-domnativemodule: 2f4b279acdb2963736fb5de2f585811dd90070b5 - React-Fabric: 6f8d1a303c96f1d078c14d74c4005bf457e5b782 - React-FabricComponents: b106410970e9a0c4e592da656c7a7e0947306c23 - React-FabricImage: 1abaf230dfce9b58fdf53c4128f3f40c6e64af6a - React-featureflags: f7ef58d91079efde3ad223bcca6d197e845d5bcf - React-featureflagsnativemodule: ae5abc9849d1696f4f8f11ee3744bf5715e032cf - React-graphics: b306856c6ed9aac32f717a229550406a53b28a6d - React-hermes: b6edce8fa19388654b1aea30844497cbeade83bc - React-idlecallbacksnativemodule: cb386712842cb9e479c89311edb234d529b64db4 - React-ImageManager: 8ce94417853eaa22faaad1f4cc1952dd3f8e2275 - React-jserrorhandler: ab827d67dc270a9c8703eef524230baeafaf6876 - React-jsi: 545342ec5c78ab1277af5f0dbe8d489e7e73db14 - React-jsiexecutor: 20210891c7c77255c16dec6762faf68b373f9f74 - React-jsinspector: 4e73460e488132d70d2b4894e5578cc856f2cb74 - React-jsinspectorcdp: 8b2bcb5779289cb2b9ca517f2965ed23eb2fd3e0 - React-jsinspectornetwork: b5e0cb9e488d294eed2d8209dc3dc0f9587210c1 - React-jsinspectortracing: f3c4036e7b984405ac910f878576d325dd9f2834 - React-jsitooling: 75bbfd221b6173a5e848ca5a6680506bac064a56 - React-jsitracing: 11ed7d821864dd988c159d4943e0a1e0937c11b1 - React-logger: 984ebd897afad067555d081deaf03f57c4315723 - React-Mapbuffer: 0c045c844ce6d85cde53e85ab163294c6adad349 - React-microtasksnativemodule: d9499269ad1f484ae71319bac1d9231447f2094e - React-NativeModulesApple: 983f3483ef0a3446b56d490f09d579fba2442e17 - React-oscompat: 114036cd8f064558c9c1a0c04fc9ae5e1453706a - React-perflogger: e7287fee27c16e3c8bd4d470f2361572b63be16b - React-performancetimeline: 8ebbaa31d2d0cea680b0a2a567500d3cab8954fc - React-RCTActionSheet: 68c68b0a7a5d2b0cfc255c64889b6e485974e988 - React-RCTAnimation: d6c5c728b888a967ce9aff1ff71a8ed71a68d069 - React-RCTAppDelegate: 0fc048666bda159cd469a6fb9befb04b3fa62be4 - React-RCTBlob: 12d8c699a1f906840113ee8d8bb575e69a05509f - React-RCTFabric: 01e815845ebc185f44205dcbf50eeb712fec23fe - React-RCTFBReactNativeSpec: f57927fb0af6ce2f25c19f8b894e2986138aa89f - React-RCTImage: a82518168f4ee407913b23ca749ca79ef51959f3 - React-RCTLinking: 7f343b584c36f024f390fea563483568fe763ef6 - React-RCTNetwork: 3165eb757ceb62a7cde4cdad043d63314122e8a3 - React-RCTRuntime: feee590c459c4cb6aaa7a00f3abc8c04709b536f - React-RCTSettings: 6bad0ae45d8d872c873059f332f586f99875621f - React-RCTText: 657d60f35983062de8f0cea67c279aa7a3ea9858 - React-RCTVibration: 78f4770515141efb7f55f9b27c49dda95319c3a8 - React-rendererconsistency: f7baab26c6d0cd5b2eb7afcecfd2d8b957017b18 - React-renderercss: bdd2f83a4a054c3e4321fd61305c202b848e471b - React-rendererdebug: 9f8865ee038127a9d99d4b034c9da4935d204993 - React-rncore: f7438473c4c71ee1963fb06a8635bb96013c9e1c - React-RuntimeApple: 4d2ab9f72b9193da86eceded128a67254fc18aeb - React-RuntimeCore: 5fd73030438d094975ca0f549d162dd97746ae38 - React-runtimeexecutor: 17c70842d5e611130cb66f91e247bc4a609c3508 - React-RuntimeHermes: 3c88e6e1ea7ea0899dcffc77c10d61ea46688cfd - React-runtimescheduler: 024500621c7c93d65371498abb4ee26d34f5d47d - React-Sandbox: e3cf3c955559ed9f0bf014b29dce1e94600cd790 - React-timing: c3c923df2b86194e1682e01167717481232f1dc7 - React-utils: 9154a037543147e1c24098f1a48fc8472602c092 - ReactAppDependencyProvider: afd905e84ee36e1678016ae04d7370c75ed539be - ReactCodegen: 06bf9ae2e01a2416250cf5e44e4a06b1c9ea201b - ReactCommon: 17fd88849a174bf9ce45461912291aca711410fc - ReactNativeFileAccess: f63160ff4e203afa99e04d9215c2ab946748b9e0 + RCTDeprecation: f17e2ebc07876ca9ab8eb6e4b0a4e4647497ae3a + RCTRequired: e2c574c1b45231f7efb0834936bd609d75072b63 + RCTTypeSafety: c693294e3993056955c3010eb1ebc574f1fcded6 + React: aeece948ccf155182ea86a2395786ed31cf21c61 + React-callinvoker: 05ad789505922d68c06cde1c8060e734df9fe182 + React-Core: 956ac86b4d9b0c0fd9a14b9cc533aa297bb501c0 + React-CoreModules: 3a8d39778cf9eeca40e419814e875da1a8e29855 + React-cxxreact: db275765e1eb08f038599fb44114cf57ee0d18cd + React-debug: 1dfa1d1cbd93bdaffa3b140190829f9fd9e27985 + React-defaultsnativemodule: 35f353ba06901fb5e374bc56e750fde05cbb05b9 + React-domnativemodule: cf9e1b1b520ce0e66396c2744b3eb6d419711c13 + React-Fabric: c0b0c1ad70476d354b3da9fef96094f7b37804da + React-FabricComponents: 8c6861c5233cf0d5685cee301a979313090e2f57 + React-FabricImage: cef8883d2fb6c892003fefcad261d2898adbe926 + React-featureflags: 0e2b969019c2b118de64a6d4c55ef7c05f2b0f1d + React-featureflagsnativemodule: e1ef619d14fe0a68d4783b32293309dbb13ef2a5 + React-graphics: 0fc6b7acaff7161bda05bf8bffceacc2b0b4e38d + React-hermes: b454b9352bc26e638704d103009f659a125b86d3 + React-idlecallbacksnativemodule: 35ab292f8404c469744db5a5dd5f0f27f95e5ebf + React-ImageManager: 3312c550ebcf6b7d911d9993082adcb3e1407ce8 + React-jserrorhandler: 2a7f2d94566f05f8cb82288afd46bc0fd8b2ffc7 + React-jsi: 7aa265cf8372d8385ccc7935729e76d27e694dfe + React-jsiexecutor: 8dd53bebfb3bc12f0541282aa4c858a433914e37 + React-jsinspector: f89b9ae62a4e2f6035b452442ef20a7f98f9cb27 + React-jsinspectorcdp: 44e46c1473a8deecf7b188389ed409be83fb3cc7 + React-jsinspectornetwork: dc9524f6e3d7694b1b6f4bd22dedad8ccc2c0a80 + React-jsinspectortracing: 0166ebbdfb125936a5d231895de3c11a19521dfc + React-jsitooling: 34692514ec8d8735938eda3677808a58f41c925b + React-jsitracing: a598dae84a87f8013635d09c5e7884023bda8501 + React-logger: 500f2fa5697d224e63c33d913c8a4765319e19bf + React-Mapbuffer: 06d59c448da7e34eb05b3fb2189e12f6a30fec57 + React-microtasksnativemodule: d1ee999dc9052e23f6488b730fa2d383a4ea40e5 + React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0 + React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb + React-perflogger: 2e229bf33e42c094fd64516d89ec1187a2b79b5b + React-performancecdpmetrics: 05ba4bd83f36acf192071bb5d9c8f45faf04d140 + React-performancetimeline: bfc96fcd2b79f7489dd54e3df4cba186dd8dd141 + React-RCTActionSheet: 2399bb6cc8adaef2e5850878102fea2ad1788a0e + React-RCTAnimation: d1deb6946e83e22a795a7d0148b94faad8851644 + React-RCTAppDelegate: 10b35d5cec3f8653f6de843ae800b3ba8050b801 + React-RCTBlob: 85150378edc42862d7c13ff2502693f32b174f91 + React-RCTFabric: 736f9da3ad57e2cef5fa4c132999933a89bb8378 + React-RCTFBReactNativeSpec: 705ec584758966950a31fa235539b57523059837 + React-RCTImage: bb6cbdc22698b3afc8eb8d81ef03ee840d24c6f6 + React-RCTLinking: e8b006d101c45651925de3e82189f03449eedfe7 + React-RCTNetwork: 7999731af05ec8f591cbc6ad4e29d79e209c581a + React-RCTRuntime: 99d8a2a17747866fb972561cdb205afe9b26d369 + React-RCTSettings: 839f334abb92e917bc24322036081ffe15c84086 + React-RCTText: 272f60e9a5dbfd14c348c85881ee7d5c7749a67c + React-RCTVibration: 1ffa30a21e2227be3afe28d657ac8e6616c91bae + React-rendererconsistency: 3c3e198aba0255524ed7126aa812d22ce750d217 + React-renderercss: 6b3ce3dfadf991937ae3229112be843ef1438c32 + React-rendererdebug: baf9e1daa07ac7f9aca379555126d29f963ba38b + React-RuntimeApple: 4136aee89257894955ef09e9f9ef74f0c27596be + React-RuntimeCore: e9a743d7de4bbd741b16e10b26078d815d6513ab + React-runtimeexecutor: 781e292362066af82fa2478d95c6b0e374421844 + React-RuntimeHermes: 6ab3c2847516769fc860d711814f1735859cad74 + React-runtimescheduler: 824c83a5fd68b35396de6d4f2f9ae995daac861b + React-Sandbox: 3936eaaeb036a782b8cd851869216c1bad0c15e9 + React-timing: 1ebc7102dd52a3edcc63534686bb156e12648411 + React-utils: abf37b162f560cd0e3e5d037af30bb796512246d + React-webperformancenativemodule: 50a57c713a90d27ae3ab947a6c9c8859bcb49709 + ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176 + ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9 + ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424 + ReactNativeFileAccess: 79b43f0419f1f602adc402b493334148de0e6a23 + RNCAsyncStorage: 29f0230e1a25f36c20b05f65e2eb8958d6526e82 RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: daa1e4de4b971b977b23bc842aaa3e135324f1f3 + Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c PODFILE CHECKSUM: 76e4115cdea15fa34db7c3b4a004fed7753cd0b7 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/apps/fs-experiment/ios/SandboxedFileAccess.h b/apps/fs-experiment/ios/SandboxedFileAccess.h new file mode 100644 index 0000000..e6950ab --- /dev/null +++ b/apps/fs-experiment/ios/SandboxedFileAccess.h @@ -0,0 +1,24 @@ +/** + * Sandboxed FileAccess implementation for react-native-sandbox. + * + * Wraps the react-native-file-access module interface, scoping all file + * operations to a per-origin sandbox directory. Constants (DocumentDir, + * CacheDir, etc.) are overridden to point into the sandbox root. + */ + +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED +#import + +@interface SandboxedFileAccess : RCTEventEmitter +#else +#import + +@interface SandboxedFileAccess : RCTEventEmitter +#endif + +@property (nonatomic, copy) NSString *sandboxRoot; + +@end diff --git a/apps/fs-experiment/ios/SandboxedFileAccess.mm b/apps/fs-experiment/ios/SandboxedFileAccess.mm new file mode 100644 index 0000000..1334668 --- /dev/null +++ b/apps/fs-experiment/ios/SandboxedFileAccess.mm @@ -0,0 +1,634 @@ +/** + * Sandboxed FileAccess β€” jails all file paths to a per-origin directory. + * + * Implements the NativeFileAccessSpec interface so JS code using + * react-native-file-access works transparently inside a sandbox. + */ + +#import "SandboxedFileAccess.h" + +#import +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#endif + +@implementation SandboxedFileAccess { + NSString *_documentsDir; + NSString *_cachesDir; + NSString *_libraryDir; + BOOL _configured; +} + +RCT_EXPORT_MODULE(SandboxedFileAccess) + ++ (BOOL)requiresMainQueueSetup { return NO; } + +- (NSArray *)supportedEvents +{ + return @[@"FetchResult"]; +} + +#pragma mark - Sandbox setup + +- (void)_setupDirectoriesForOrigin:(NSString *)origin +{ + NSString *appSupport = NSSearchPathForDirectoriesInDomains( + NSApplicationSupportDirectory, NSUserDomainMask, YES).firstObject; + NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier] ?: @"com.unknown"; + _sandboxRoot = [[[appSupport stringByAppendingPathComponent:bundleId] + stringByAppendingPathComponent:@"Sandboxes"] + stringByAppendingPathComponent:origin]; + + _documentsDir = [_sandboxRoot stringByAppendingPathComponent:@"Documents"]; + _cachesDir = [_sandboxRoot stringByAppendingPathComponent:@"Caches"]; + _libraryDir = [_sandboxRoot stringByAppendingPathComponent:@"Library"]; + + NSFileManager *fm = [NSFileManager defaultManager]; + for (NSString *dir in @[_documentsDir, _cachesDir, _libraryDir]) { + [fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil]; + } + + _configured = YES; +} + +#pragma mark - RCTSandboxAwareModule + +- (void)configureSandboxWithOrigin:(NSString *)origin + requestedName:(NSString *)requestedName + resolvedName:(NSString *)resolvedName +{ + if (!origin) { + NSLog(@"[SandboxedFileAccess] ERROR: origin is nil, refusing to configure"); + return; + } + NSLog(@"[SandboxedFileAccess] Configuring for origin '%@'", origin); + [self _setupDirectoriesForOrigin:origin]; +} + +#pragma mark - Path validation + +- (nullable NSString *)_sandboxedPath:(NSString *)path + reject:(RCTPromiseRejectBlock)reject +{ + if (!_configured) { + reject(@"EPERM", @"SandboxedFileAccess: sandbox not configured. " + "configureSandboxWithOrigin: must be called before any file operation.", nil); + return nil; + } + + NSString *resolved; + if ([path hasPrefix:@"/"]) { + resolved = [path stringByStandardizingPath]; + } else { + resolved = [[_documentsDir stringByAppendingPathComponent:path] stringByStandardizingPath]; + } + + if ([resolved hasPrefix:_sandboxRoot]) { + return resolved; + } + + reject(@"EPERM", [NSString stringWithFormat: + @"Path '%@' is outside the sandbox. Allowed root: %@", path, _sandboxRoot], nil); + return nil; +} + +#pragma mark - Constants + +#ifdef RCT_NEW_ARCH_ENABLED +- (facebook::react::ModuleConstants)constantsToExport +{ + return [self getConstants]; +} + +- (facebook::react::ModuleConstants)getConstants +{ + if (!_configured) { + return facebook::react::typedConstants({ + .CacheDir = @"", + .DocumentDir = @"", + .LibraryDir = @"", + .MainBundleDir = @"", + }); + } + return facebook::react::typedConstants({ + .CacheDir = _cachesDir, + .DocumentDir = _documentsDir, + .LibraryDir = _libraryDir, + .MainBundleDir = _documentsDir, + }); +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#else +- (NSDictionary *)constantsToExport +{ + if (!_configured) { + return @{}; + } + return @{ + @"CacheDir": _cachesDir, + @"DocumentDir": _documentsDir, + @"LibraryDir": _libraryDir, + @"MainBundleDir": _documentsDir, + }; +} +#endif + +#pragma mark - File operations + +RCT_EXPORT_METHOD(writeFile:(NSString *)path + data:(NSString *)data + encoding:(NSString *)encoding + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + if ([encoding isEqualToString:@"base64"]) { + NSData *decoded = [[NSData alloc] initWithBase64EncodedString:data + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + if (!decoded) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to write to '%@', invalid base64.", path], nil); + return; + } + [decoded writeToFile:safePath options:NSDataWritingAtomic error:&error]; + } else { + [data writeToFile:safePath atomically:NO encoding:NSUTF8StringEncoding error:&error]; + } + if (error) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to write to '%@'. %@", path, error.localizedDescription], error); + } else { + resolve(nil); + } + }); +} + +RCT_EXPORT_METHOD(readFile:(NSString *)path + encoding:(NSString *)encoding + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + if ([encoding isEqualToString:@"base64"]) { + NSData *data = [NSData dataWithContentsOfFile:safePath options:0 error:&error]; + if (error || !data) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to read '%@'. %@", path, + error.localizedDescription ?: @""], error); + return; + } + resolve([data base64EncodedStringWithOptions:0]); + } else { + NSString *content = [NSString stringWithContentsOfFile:safePath encoding:NSUTF8StringEncoding error:&error]; + if (error) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to read '%@'. %@", path, error.localizedDescription], error); + return; + } + resolve(content); + } + }); +} + +RCT_EXPORT_METHOD(readFileChunk:(NSString *)path + offset:(double)offset + length:(double)length + encoding:(NSString *)encoding + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + NSFileHandle *fh = [NSFileHandle fileHandleForReadingFromURL: + [NSURL fileURLWithPath:safePath] error:&error]; + if (error || !fh) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to read '%@'. %@", path, + error.localizedDescription ?: @""], error); + return; + } + + [fh seekToFileOffset:(unsigned long long)offset]; + NSData *data = [fh readDataOfLength:(NSUInteger)length]; + [fh closeFile]; + + if ([encoding isEqualToString:@"base64"]) { + resolve([data base64EncodedStringWithOptions:0]); + } else { + NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (content) { + resolve(content); + } else { + reject(@"ERR", @"Failed to decode content with specified encoding.", nil); + } + } + }); +} + +RCT_EXPORT_METHOD(appendFile:(NSString *)path + data:(NSString *)data + encoding:(NSString *)encoding + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *encoded = [encoding isEqualToString:@"base64"] + ? [[NSData alloc] initWithBase64EncodedString:data options:NSDataBase64DecodingIgnoreUnknownCharacters] + : [data dataUsingEncoding:NSUTF8StringEncoding]; + + NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:safePath]; + if (!fh) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to append to '%@'.", path], nil); + return; + } + [fh seekToEndOfFile]; + [fh writeData:encoded]; + [fh closeFile]; + resolve(nil); + }); +} + +RCT_EXPORT_METHOD(exists:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + resolve(@([[NSFileManager defaultManager] fileExistsAtPath:safePath])); + }); +} + +RCT_EXPORT_METHOD(isDir:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + BOOL isDir = NO; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:safePath isDirectory:&isDir]; + resolve(@(exists && isDir)); + }); +} + +RCT_EXPORT_METHOD(ls:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:safePath error:&error]; + if (error) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to list '%@'. %@", path, error.localizedDescription], error); + return; + } + resolve(contents); + }); +} + +RCT_EXPORT_METHOD(mkdir:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + if (![[NSFileManager defaultManager] createDirectoryAtPath:safePath + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to create '%@'. %@", path, error.localizedDescription], error); + return; + } + resolve(safePath); + }); +} + +RCT_EXPORT_METHOD(cp:(NSString *)source + target:(NSString *)target + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *src = [self _sandboxedPath:source reject:reject]; + if (!src) return; + NSString *dst = [self _sandboxedPath:target reject:reject]; + if (!dst) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + if (![[NSFileManager defaultManager] copyItemAtPath:src toPath:dst error:&error]) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to copy '%@' to '%@'. %@", + source, target, error.localizedDescription], error); + return; + } + resolve(nil); + }); +} + +RCT_EXPORT_METHOD(mv:(NSString *)source + target:(NSString *)target + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *src = [self _sandboxedPath:source reject:reject]; + if (!src) return; + NSString *dst = [self _sandboxedPath:target reject:reject]; + if (!dst) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:dst error:nil]; + if (![[NSFileManager defaultManager] moveItemAtPath:src toPath:dst error:&error]) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to move '%@' to '%@'. %@", + source, target, error.localizedDescription], error); + return; + } + resolve(nil); + }); +} + +RCT_EXPORT_METHOD(unlink:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + if (![[NSFileManager defaultManager] removeItemAtPath:safePath error:&error]) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to unlink '%@'. %@", path, error.localizedDescription], error); + return; + } + resolve(nil); + }); +} + +RCT_EXPORT_METHOD(stat:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:safePath error:&error]; + if (error) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to stat '%@'. %@", path, error.localizedDescription], error); + return; + } + + NSURL *pathUrl = [NSURL fileURLWithPath:safePath]; + BOOL isDir = NO; + [[NSFileManager defaultManager] fileExistsAtPath:safePath isDirectory:&isDir]; + + resolve(@{ + @"filename": pathUrl.lastPathComponent ?: @"", + @"lastModified": @(1000.0 * [(NSDate *)attrs[NSFileModificationDate] timeIntervalSince1970]), + @"path": safePath, + @"size": attrs[NSFileSize] ?: @0, + @"type": isDir ? @"directory" : @"file", + }); + }); +} + +RCT_EXPORT_METHOD(statDir:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:safePath error:&error]; + if (error) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to list '%@'. %@", path, error.localizedDescription], error); + return; + } + + NSMutableArray *results = [NSMutableArray new]; + for (NSString *name in contents) { + NSString *fullPath = [safePath stringByAppendingPathComponent:name]; + NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil]; + if (!attrs) continue; + + BOOL isDir = NO; + [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDir]; + + [results addObject:@{ + @"filename": name, + @"lastModified": @(1000.0 * [(NSDate *)attrs[NSFileModificationDate] timeIntervalSince1970]), + @"path": fullPath, + @"size": attrs[NSFileSize] ?: @0, + @"type": isDir ? @"directory" : @"file", + }]; + } + resolve(results); + }); +} + +RCT_EXPORT_METHOD(hash:(NSString *)path + algorithm:(NSString *)algorithm + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *safePath = [self _sandboxedPath:path reject:reject]; + if (!safePath) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *data = [NSData dataWithContentsOfFile:safePath]; + if (!data) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to read '%@'.", path], nil); + return; + } + + unsigned char buffer[CC_SHA512_DIGEST_LENGTH]; + int digestLength; + + if ([algorithm isEqualToString:@"MD5"]) { + digestLength = CC_MD5_DIGEST_LENGTH; + CC_MD5(data.bytes, (CC_LONG)data.length, buffer); + } else if ([algorithm isEqualToString:@"SHA-1"]) { + digestLength = CC_SHA1_DIGEST_LENGTH; + CC_SHA1(data.bytes, (CC_LONG)data.length, buffer); + } else if ([algorithm isEqualToString:@"SHA-256"]) { + digestLength = CC_SHA256_DIGEST_LENGTH; + CC_SHA256(data.bytes, (CC_LONG)data.length, buffer); + } else if ([algorithm isEqualToString:@"SHA-512"]) { + digestLength = CC_SHA512_DIGEST_LENGTH; + CC_SHA512(data.bytes, (CC_LONG)data.length, buffer); + } else { + reject(@"ERR", [NSString stringWithFormat:@"Unknown algorithm '%@'.", algorithm], nil); + return; + } + + NSMutableString *output = [NSMutableString stringWithCapacity:digestLength * 2]; + for (int i = 0; i < digestLength; i++) { + [output appendFormat:@"%02x", buffer[i]]; + } + resolve(output); + }); +} + +RCT_EXPORT_METHOD(concatFiles:(NSString *)source + target:(NSString *)target + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *src = [self _sandboxedPath:source reject:reject]; + if (!src) return; + NSString *dst = [self _sandboxedPath:target reject:reject]; + if (!dst) return; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSInputStream *input = [NSInputStream inputStreamWithFileAtPath:src]; + NSOutputStream *output = [NSOutputStream outputStreamToFileAtPath:dst append:YES]; + if (!input || !output) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to concat '%@' to '%@'.", source, target], nil); + return; + } + + [input open]; + [output open]; + NSInteger totalBytes = 0; + uint8_t buf[8192]; + NSInteger len; + while ((len = [input read:buf maxLength:sizeof(buf)]) > 0) { + [output write:buf maxLength:len]; + totalBytes += len; + } + [output close]; + [input close]; + resolve(@(totalBytes)); + }); +} + +RCT_EXPORT_METHOD(df:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + NSDictionary *attrs = [[NSFileManager defaultManager] + attributesOfFileSystemForPath:self->_sandboxRoot error:&error]; + if (error) { + reject(@"ERR", [NSString stringWithFormat:@"Failed to stat filesystem. %@", error.localizedDescription], error); + return; + } + resolve(@{ + @"internal_free": attrs[NSFileSystemFreeSize], + @"internal_total": attrs[NSFileSystemSize], + }); + }); +} + +#pragma mark - Blocked operations + +#ifdef RCT_NEW_ARCH_ENABLED +RCT_EXPORT_METHOD(fetch:(double)requestId + resource:(NSString *)resource + init:(JS::NativeFileAccess::SpecFetchInit &)init) +{ + RCTLogWarn(@"[SandboxedFileAccess] fetch is not available in sandboxed mode"); +} +#else +RCT_EXPORT_METHOD(fetch:(double)requestId + resource:(NSString *)resource + init:(NSDictionary *)init) +{ + RCTLogWarn(@"[SandboxedFileAccess] fetch is not available in sandboxed mode"); +} +#endif + +RCT_EXPORT_METHOD(cancelFetch:(double)requestId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + resolve(nil); +} + +RCT_EXPORT_METHOD(cpAsset:(NSString *)asset + target:(NSString *)target + type:(NSString *)type + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"cpAsset is not available in sandboxed mode", nil); +} + +RCT_EXPORT_METHOD(cpExternal:(NSString *)source + targetName:(NSString *)targetName + dir:(NSString *)dir + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"cpExternal is not available in sandboxed mode", nil); +} + +RCT_EXPORT_METHOD(getAppGroupDir:(NSString *)groupName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"getAppGroupDir is not available in sandboxed mode", nil); +} + +RCT_EXPORT_METHOD(unzip:(NSString *)source + target:(NSString *)target + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSString *src = [self _sandboxedPath:source reject:reject]; + if (!src) return; + NSString *dst = [self _sandboxedPath:target reject:reject]; + if (!dst) return; + + reject(@"EPERM", @"unzip is not available in sandboxed mode", nil); +} + +RCT_EXPORT_METHOD(hardlink:(NSString *)source + target:(NSString *)target + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"hardlink is not available in sandboxed mode", nil); +} + +RCT_EXPORT_METHOD(symlink:(NSString *)source + target:(NSString *)target + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"symlink is not available in sandboxed mode", nil); +} + +// Required by RCTEventEmitter +RCT_EXPORT_METHOD(addListener:(NSString *)eventName) {} +RCT_EXPORT_METHOD(removeListeners:(double)count) {} + +@end diff --git a/apps/fs-experiment/ios/SandboxedRNCAsyncStorage.h b/apps/fs-experiment/ios/SandboxedRNCAsyncStorage.h new file mode 100644 index 0000000..30ea798 --- /dev/null +++ b/apps/fs-experiment/ios/SandboxedRNCAsyncStorage.h @@ -0,0 +1,56 @@ +/** + * Sandboxed AsyncStorage implementation for react-native-sandbox. + * + * Based on RNCAsyncStorage from @react-native-async-storage/async-storage, + * adapted to scope storage per sandbox origin. This module is intended to be + * used as a TurboModule substitution target via turboModuleSubstitutions. + * + * When the sandbox requests "RNCAsyncStorage", this module can be resolved + * instead, providing isolated key-value storage per sandbox origin. + */ + +#import + +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#endif + +#import "RNCAsyncStorageDelegate.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SandboxedRNCAsyncStorage : NSObject < +#ifdef RCT_NEW_ARCH_ENABLED + NativeAsyncStorageModuleSpec +#else + RCTBridgeModule +#endif + , + RCTInvalidating, + RCTSandboxAwareModule> + +@property (nonatomic, weak, nullable) id delegate; +@property (nonatomic, assign) BOOL clearOnInvalidate; +@property (nonatomic, readonly, getter=isValid) BOOL valid; + +/** + * The storage directory for this instance. When created via default init, + * this defaults to a "SandboxedAsyncStorage/default" directory. + * The sandbox delegate's configureSandbox will update the storageDirectory + * based on the sandbox origin BEFORE any storage operations are performed. + */ +@property (nonatomic, copy) NSString *storageDirectory; + +- (instancetype)initWithStorageDirectory:(NSString *)storageDirectory; +- (void)clearAllData; +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)multiSet:(NSArray *> *)kvPairs callback:(RCTResponseSenderBlock)callback; +- (void)getAllKeys:(RCTResponseSenderBlock)callback; + +@end + +NS_ASSUME_NONNULL_END diff --git a/apps/fs-experiment/ios/SandboxedRNCAsyncStorage.mm b/apps/fs-experiment/ios/SandboxedRNCAsyncStorage.mm new file mode 100644 index 0000000..a42ca05 --- /dev/null +++ b/apps/fs-experiment/ios/SandboxedRNCAsyncStorage.mm @@ -0,0 +1,625 @@ +/** + * Sandboxed AsyncStorage implementation for react-native-sandbox. + * + * Based on the original RNCAsyncStorage from @react-native-async-storage/async-storage. + * Scopes all storage to a per-origin directory to prevent data leaks between sandboxes. + */ + +#import "SandboxedRNCAsyncStorage.h" + +#import +#import +#import + +static NSString *const RCTManifestFileName = @"manifest.json"; +static const NSUInteger RCTInlineValueThreshold = 1024; + +#pragma mark - Static helper functions + +static NSDictionary *RCTErrorForKey(NSString *key) +{ + if (![key isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key}); + } else if (key.length < 1) { + return RCTMakeAndLogError( + @"Invalid key - must be at least one character. Key: ", key, @{@"key": key}); + } else { + return nil; + } +} + +static void RCTAppendError(NSDictionary *error, NSMutableArray **errors) +{ + if (error && errors) { + if (!*errors) { + *errors = [NSMutableArray new]; + } + [*errors addObject:error]; + } +} + +static NSArray *RCTMakeErrors(NSArray> *results) +{ + NSMutableArray *errors; + for (id object in results) { + if ([object isKindOfClass:[NSError class]]) { + NSError *error = (NSError *)object; + NSDictionary *keyError = RCTMakeError(error.localizedDescription, error, nil); + RCTAppendError(keyError, &errors); + } + } + return errors; +} + +static NSString *RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) +{ + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + NSError *error; + NSStringEncoding encoding; + NSString *entryString = [NSString stringWithContentsOfFile:filePath + usedEncoding:&encoding + error:&error]; + NSDictionary *extraData = @{@"key": RCTNullIfNil(key)}; + + if (error) { + if (errorOut) { + *errorOut = RCTMakeError(@"Failed to read storage file.", error, extraData); + } + return nil; + } + + if (encoding != NSUTF8StringEncoding) { + if (errorOut) { + *errorOut = + RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), extraData); + } + return nil; + } + return entryString; + } + + return nil; +} + +static BOOL RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) +{ + BOOL modified = NO; + for (NSString *key in source) { + id sourceValue = source[key]; + id destinationValue = destination[key]; + if ([sourceValue isKindOfClass:[NSDictionary class]]) { + if ([destinationValue isKindOfClass:[NSDictionary class]]) { + if ([destinationValue classForCoder] != [NSMutableDictionary class]) { + destinationValue = [destinationValue mutableCopy]; + } + if (RCTMergeRecursive(destinationValue, sourceValue)) { + destination[key] = destinationValue; + modified = YES; + } + } else { + destination[key] = [sourceValue copy]; + modified = YES; + } + } else if (![source isEqual:destinationValue]) { + destination[key] = [sourceValue copy]; + modified = YES; + } + } + return modified; +} + +#define RCTGetStorageDirectory() _storageDirectory +#define RCTGetManifestFilePath() _manifestFilePath +#define RCTGetMethodQueue() self.methodQueue +#define RCTGetCache() self.cache + +static NSDictionary *RCTDeleteStorageDirectory(NSString *storageDirectory) +{ + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:storageDirectory error:&error]; + return error ? RCTMakeError(@"Failed to delete storage directory.", error, nil) : nil; +} + +#define RCTDeleteStorageDirectory() RCTDeleteStorageDirectory(_storageDirectory) + +#pragma mark - SandboxedRNCAsyncStorage + +@interface SandboxedRNCAsyncStorage () + +@property (nonatomic, copy) NSString *manifestFilePath; + +@end + +@implementation SandboxedRNCAsyncStorage { + BOOL _haveSetup; + BOOL _configured; + NSMutableDictionary *_manifest; + NSCache *_cache; + dispatch_once_t _cacheOnceToken; +} + +RCT_EXPORT_MODULE(SandboxedAsyncStorage) + +- (instancetype)initWithStorageDirectory:(NSString *)storageDirectory +{ + if ((self = [super init])) { + _storageDirectory = storageDirectory; + _manifestFilePath = [_storageDirectory stringByAppendingPathComponent:RCTManifestFileName]; + _configured = YES; + } + return self; +} + +@synthesize methodQueue = _methodQueue; + +- (void)setStorageDirectory:(NSString *)storageDirectory +{ + _storageDirectory = [storageDirectory copy]; + _manifestFilePath = [_storageDirectory stringByAppendingPathComponent:RCTManifestFileName]; + _haveSetup = NO; + [_manifest removeAllObjects]; + [_cache removeAllObjects]; +} + +- (NSCache *)cache +{ + dispatch_once(&_cacheOnceToken, ^{ + _cache = [NSCache new]; + _cache.totalCostLimit = 2 * 1024 * 1024; // 2MB + + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationDidReceiveMemoryWarningNotification + object:nil + queue:nil + usingBlock:^(__unused NSNotification *note) { + [self->_cache removeAllObjects]; + }]; + }); + return _cache; +} + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +- (instancetype)init +{ + if ((self = [super init])) { + _configured = NO; + } + return self; +} + +- (void)clearAllData +{ + dispatch_async(RCTGetMethodQueue(), ^{ + [self->_manifest removeAllObjects]; + [RCTGetCache() removeAllObjects]; + RCTDeleteStorageDirectory(); + }); +} + +- (void)invalidate +{ + if (_clearOnInvalidate) { + [RCTGetCache() removeAllObjects]; + RCTDeleteStorageDirectory(); + } + _clearOnInvalidate = NO; + [_manifest removeAllObjects]; + _haveSetup = NO; +} + +- (BOOL)isValid +{ + return _haveSetup; +} + +- (void)dealloc +{ + [self invalidate]; +} + +- (NSString *)_filePathForKey:(NSString *)key +{ + NSString *safeFileName = RCTMD5Hash(key); + return [RCTGetStorageDirectory() stringByAppendingPathComponent:safeFileName]; +} + +- (NSDictionary *)_ensureSetup +{ + RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); + + if (!_configured) { + return RCTMakeError(@"SandboxedAsyncStorage: sandbox not configured. " + "configureSandboxWithOrigin: must be called before any storage operation.", nil, nil); + } + + NSError *error = nil; + [[NSFileManager defaultManager] createDirectoryAtPath:RCTGetStorageDirectory() + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + return RCTMakeError(@"Failed to create storage directory.", error, nil); + } + + if (!_haveSetup) { + NSDictionary *errorOut = nil; + NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), RCTManifestFileName, &errorOut); + if (!serialized) { + if (errorOut) { + RCTLogError(@"Could not open the existing manifest: %@", errorOut); + return errorOut; + } else { + _manifest = [NSMutableDictionary new]; + } + } else { + _manifest = RCTJSONParseMutable(serialized, &error); + if (!_manifest) { + RCTLogError(@"Failed to parse manifest - creating a new one: %@", error); + _manifest = [NSMutableDictionary new]; + } + } + _haveSetup = YES; + } + + return nil; +} + +- (NSDictionary *)_writeManifest:(NSMutableArray *__autoreleasing *)errors +{ + NSError *error; + NSString *serialized = RCTJSONStringify(_manifest, &error); + [serialized writeToFile:RCTGetManifestFilePath() + atomically:YES + encoding:NSUTF8StringEncoding + error:&error]; + NSDictionary *errorOut; + if (error) { + errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); + RCTAppendError(errorOut, errors); + } + return errorOut; +} + +- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary *__autoreleasing *)errorOut +{ + NSString *value = _manifest[key]; + if (value == (id)kCFNull) { + value = [RCTGetCache() objectForKey:key]; + if (!value) { + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, errorOut); + if (value) { + [RCTGetCache() setObject:value forKey:key cost:value.length]; + } else { + [_manifest removeObjectForKey:key]; + } + } + } + return value; +} + +- (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL *)changedManifest +{ + if (entry.count != 2) { + return RCTMakeAndLogError( + @"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); + } + NSString *key = entry[0]; + NSDictionary *errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + NSString *value = entry[1]; + NSString *filePath = [self _filePathForKey:key]; + NSError *error; + if (value.length <= RCTInlineValueThreshold) { + if (_manifest[key] == (id)kCFNull) { + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [RCTGetCache() removeObjectForKey:key]; + } + *changedManifest = YES; + _manifest[key] = value; + return nil; + } + [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + [RCTGetCache() setObject:value forKey:key cost:value.length]; + if (error) { + errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); + } else if (_manifest[key] != (id)kCFNull) { + *changedManifest = YES; + _manifest[key] = (id)kCFNull; + } + return errorOut; +} + +- (void)_multiGet:(NSArray *)keys + callback:(RCTResponseSenderBlock)callback + getter:(NSString * (^)(NSUInteger i, NSString *key, NSDictionary **errorOut))getValue +{ + NSMutableArray *errors; + NSMutableArray *> *result = [NSMutableArray arrayWithCapacity:keys.count]; + for (NSUInteger i = 0; i < keys.count; ++i) { + NSString *key = keys[i]; + id keyError; + id value = getValue(i, key, &keyError); + [result addObject:@[key, RCTNullIfNil(value)]]; + RCTAppendError(keyError, &errors); + } + callback(@[RCTNullIfNil(errors), result]); +} + +- (BOOL)_passthroughDelegate +{ + return + [self.delegate respondsToSelector:@selector(isPassthrough)] && self.delegate.isPassthrough; +} + +#pragma mark - Exported JS Functions + +// clang-format off +RCT_EXPORT_METHOD(multiGet:(NSArray *)keys + callback:(RCTResponseSenderBlock)callback) +// clang-format on +{ + if (self.delegate != nil) { + [self.delegate + valuesForKeys:keys + completion:^(NSArray> *valuesOrErrors) { + [self _multiGet:keys + callback:callback + getter:^NSString *(NSUInteger i, NSString *key, NSDictionary **errorOut) { + id valueOrError = valuesOrErrors[i]; + if ([valueOrError isKindOfClass:[NSError class]]) { + NSError *error = (NSError *)valueOrError; + NSDictionary *extraData = @{@"key": RCTNullIfNil(key)}; + *errorOut = + RCTMakeError(error.localizedDescription, error, extraData); + return nil; + } else { + return [valueOrError isKindOfClass:[NSString class]] + ? (NSString *)valueOrError + : nil; + } + }]; + }]; + + if (![self _passthroughDelegate]) { + return; + } + } + + NSDictionary *ensureSetupErrorOut = [self _ensureSetup]; + if (ensureSetupErrorOut) { + callback(@[@[ensureSetupErrorOut], (id)kCFNull]); + return; + } + [self _multiGet:keys + callback:callback + getter:^(__unused NSUInteger i, NSString *key, NSDictionary **errorOut) { + return [self _getValueForKey:key errorOut:errorOut]; + }]; +} + +// clang-format off +RCT_EXPORT_METHOD(multiSet:(NSArray *> *)kvPairs + callback:(RCTResponseSenderBlock)callback) +// clang-format on +{ + if (self.delegate != nil) { + NSMutableArray *keys = [NSMutableArray arrayWithCapacity:kvPairs.count]; + NSMutableArray *values = [NSMutableArray arrayWithCapacity:kvPairs.count]; + for (NSArray *entry in kvPairs) { + [keys addObject:entry[0]]; + [values addObject:entry[1]]; + } + [self.delegate setValues:values + forKeys:keys + completion:^(NSArray> *results) { + NSArray *errors = RCTMakeErrors(results); + callback(@[RCTNullIfNil(errors)]); + }]; + + if (![self _passthroughDelegate]) { + return; + } + } + + NSDictionary *errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + BOOL changedManifest = NO; + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + NSDictionary *keyError = [self _writeEntry:entry changedManifest:&changedManifest]; + RCTAppendError(keyError, &errors); + } + if (changedManifest) { + [self _writeManifest:&errors]; + } + callback(@[RCTNullIfNil(errors)]); +} + +// clang-format off +RCT_EXPORT_METHOD(multiMerge:(NSArray *> *)kvPairs + callback:(RCTResponseSenderBlock)callback) +// clang-format on +{ + if (self.delegate != nil) { + NSMutableArray *keys = [NSMutableArray arrayWithCapacity:kvPairs.count]; + NSMutableArray *values = [NSMutableArray arrayWithCapacity:kvPairs.count]; + for (NSArray *entry in kvPairs) { + [keys addObject:entry[0]]; + [values addObject:entry[1]]; + } + [self.delegate mergeValues:values + forKeys:keys + completion:^(NSArray> *results) { + NSArray *errors = RCTMakeErrors(results); + callback(@[RCTNullIfNil(errors)]); + }]; + + if (![self _passthroughDelegate]) { + return; + } + } + + NSDictionary *errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + BOOL changedManifest = NO; + NSMutableArray *errors; + for (__strong NSArray *entry in kvPairs) { + NSDictionary *keyError; + NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; + if (!keyError) { + if (value) { + NSError *jsonError; + NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &jsonError); + NSDictionary *mergingValue = RCTJSONParse(entry[1], &jsonError); + if (!mergingValue.count || RCTMergeRecursive(mergedVal, mergingValue)) { + entry = @[entry[0], RCTNullIfNil(RCTJSONStringify(mergedVal, NULL))]; + } + if (jsonError) { + keyError = RCTJSErrorFromNSError(jsonError); + } + } + if (!keyError) { + keyError = [self _writeEntry:entry changedManifest:&changedManifest]; + } + } + RCTAppendError(keyError, &errors); + } + if (changedManifest) { + [self _writeManifest:&errors]; + } + callback(@[RCTNullIfNil(errors)]); +} + +// clang-format off +RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys + callback:(RCTResponseSenderBlock)callback) +// clang-format on +{ + if (self.delegate != nil) { + [self.delegate removeValuesForKeys:keys + completion:^(NSArray> *results) { + NSArray *errors = RCTMakeErrors(results); + callback(@[RCTNullIfNil(errors)]); + }]; + + if (![self _passthroughDelegate]) { + return; + } + } + + NSDictionary *errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + BOOL changedManifest = NO; + for (NSString *key in keys) { + NSDictionary *keyError = RCTErrorForKey(key); + if (!keyError) { + if (_manifest[key] == (id)kCFNull) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [RCTGetCache() removeObjectForKey:key]; + } + if (_manifest[key]) { + changedManifest = YES; + [_manifest removeObjectForKey:key]; + } + } + RCTAppendError(keyError, &errors); + } + if (changedManifest) { + [self _writeManifest:&errors]; + } + callback(@[RCTNullIfNil(errors)]); +} + +// clang-format off +RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) +// clang-format on +{ + if (self.delegate != nil) { + [self.delegate removeAllValues:^(NSError *error) { + NSDictionary *result = nil; + if (error != nil) { + result = RCTMakeError(error.localizedDescription, error, nil); + } + callback(@[RCTNullIfNil(result)]); + }]; + return; + } + + [_manifest removeAllObjects]; + [RCTGetCache() removeAllObjects]; + NSDictionary *error = RCTDeleteStorageDirectory(); + callback(@[RCTNullIfNil(error)]); +} + +// clang-format off +RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) +// clang-format on +{ + if (self.delegate != nil) { + [self.delegate allKeys:^(NSArray> *keys) { + callback(@[(id)kCFNull, keys]); + }]; + + if (![self _passthroughDelegate]) { + return; + } + } + + NSDictionary *errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[errorOut, (id)kCFNull]); + } else { + callback(@[(id)kCFNull, _manifest.allKeys]); + } +} + +#pragma mark - RCTSandboxAwareModule + +- (void)configureSandboxWithOrigin:(NSString *)origin + requestedName:(NSString *)requestedName + resolvedName:(NSString *)resolvedName +{ + if (!origin) { + NSLog(@"[SandboxedRNCAsyncStorage] ERROR: origin is nil, refusing to configure"); + return; + } + NSString *appSupport = NSSearchPathForDirectoriesInDomains( + NSApplicationSupportDirectory, NSUserDomainMask, YES).firstObject; + NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier] ?: @"com.unknown"; + NSString *scopedDir = [[[[appSupport stringByAppendingPathComponent:bundleId] + stringByAppendingPathComponent:@"Sandboxes"] + stringByAppendingPathComponent:origin] + stringByAppendingPathComponent:@"AsyncStorage"]; + + NSLog(@"[SandboxedRNCAsyncStorage] Configuring for origin '%@', storage dir: %@", origin, scopedDir); + self.storageDirectory = scopedDir; + _configured = YES; +} + +#if RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif + +@end diff --git a/apps/fs-experiment/ios/SandboxedRNFSManager.h b/apps/fs-experiment/ios/SandboxedRNFSManager.h new file mode 100644 index 0000000..8046a97 --- /dev/null +++ b/apps/fs-experiment/ios/SandboxedRNFSManager.h @@ -0,0 +1,25 @@ +/** + * Sandboxed RNFSManager implementation for react-native-sandbox. + * + * Wraps the original RNFSManager from react-native-fs, scoping all file + * operations to a per-origin sandbox directory. Exposed directory constants + * (DocumentDirectoryPath, CachesDirectoryPath, etc.) are overridden to point + * into the sandbox root. + */ + +#import + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SandboxedRNFSManager : RCTEventEmitter + +@property (nonatomic, copy) NSString *sandboxRoot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/apps/fs-experiment/ios/SandboxedRNFSManager.mm b/apps/fs-experiment/ios/SandboxedRNFSManager.mm new file mode 100644 index 0000000..1258178 --- /dev/null +++ b/apps/fs-experiment/ios/SandboxedRNFSManager.mm @@ -0,0 +1,555 @@ +/** + * Sandboxed RNFSManager β€” jails all file paths to a per-origin directory. + * + * Every path argument is validated against the sandbox root. Directory + * constants exposed to JS (RNFSDocumentDirectoryPath, etc.) are overridden. + */ + +#import "SandboxedRNFSManager.h" + +#import +#import +#import +#import + +@implementation SandboxedRNFSManager { + dispatch_queue_t _methodQueue; + NSString *_documentsDir; + NSString *_cachesDir; + NSString *_tempDir; + NSString *_libraryDir; + BOOL _configured; +} + +RCT_EXPORT_MODULE(SandboxedRNFSManager) + ++ (BOOL)requiresMainQueueSetup { return NO; } + +- (dispatch_queue_t)methodQueue +{ + if (!_methodQueue) { + _methodQueue = dispatch_queue_create("sandbox.rnfs", DISPATCH_QUEUE_SERIAL); + } + return _methodQueue; +} + +- (NSArray *)supportedEvents +{ + return @[@"DownloadBegin", @"DownloadProgress", @"DownloadResumable", + @"UploadBegin", @"UploadProgress"]; +} + +#pragma mark - Sandbox setup + +- (void)_setupDirectoriesForOrigin:(NSString *)origin +{ + NSString *appSupport = NSSearchPathForDirectoriesInDomains( + NSApplicationSupportDirectory, NSUserDomainMask, YES).firstObject; + NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier] ?: @"com.unknown"; + _sandboxRoot = [[[appSupport stringByAppendingPathComponent:bundleId] + stringByAppendingPathComponent:@"Sandboxes"] + stringByAppendingPathComponent:origin]; + + _documentsDir = [_sandboxRoot stringByAppendingPathComponent:@"Documents"]; + _cachesDir = [_sandboxRoot stringByAppendingPathComponent:@"Caches"]; + _tempDir = [_sandboxRoot stringByAppendingPathComponent:@"tmp"]; + _libraryDir = [_sandboxRoot stringByAppendingPathComponent:@"Library"]; + + NSFileManager *fm = [NSFileManager defaultManager]; + for (NSString *dir in @[_documentsDir, _cachesDir, _tempDir, _libraryDir]) { + [fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil]; + } + + _configured = YES; +} + +#pragma mark - RCTSandboxAwareModule + +- (void)configureSandboxWithOrigin:(NSString *)origin + requestedName:(NSString *)requestedName + resolvedName:(NSString *)resolvedName +{ + if (!origin) { + NSLog(@"[SandboxedRNFSManager] ERROR: origin is nil, refusing to configure"); + return; + } + NSLog(@"[SandboxedRNFSManager] Configuring for origin '%@'", origin); + [self _setupDirectoriesForOrigin:origin]; +} + +#pragma mark - Path validation + +- (nullable NSString *)_sandboxedPath:(NSString *)path + reject:(RCTPromiseRejectBlock)reject +{ + if (!_configured) { + reject(@"EPERM", @"SandboxedRNFSManager: sandbox not configured. " + "configureSandboxWithOrigin: must be called before any file operation.", nil); + return nil; + } + + NSString *resolved; + if ([path hasPrefix:@"/"]) { + resolved = [path stringByStandardizingPath]; + } else { + resolved = [[_documentsDir stringByAppendingPathComponent:path] stringByStandardizingPath]; + } + + if ([resolved hasPrefix:_sandboxRoot]) { + return resolved; + } + + reject(@"EPERM", [NSString stringWithFormat: + @"Path '%@' is outside the sandbox. Allowed root: %@", path, _sandboxRoot], nil); + return nil; +} + +- (nullable NSString *)_sandboxedSrcPath:(NSString *)path + reject:(RCTPromiseRejectBlock)reject +{ + return [self _sandboxedPath:path reject:reject]; +} + +#pragma mark - Constants + +- (NSDictionary *)constantsToExport +{ + if (!_configured) { + return @{}; + } + return @{ + @"RNFSMainBundlePath": _documentsDir, // no access to real main bundle + @"RNFSCachesDirectoryPath": _cachesDir, + @"RNFSDocumentDirectoryPath": _documentsDir, + @"RNFSExternalDirectoryPath": [NSNull null], + @"RNFSExternalStorageDirectoryPath": [NSNull null], + @"RNFSExternalCachesDirectoryPath": [NSNull null], + @"RNFSDownloadDirectoryPath": [NSNull null], + @"RNFSTemporaryDirectoryPath": _tempDir, + @"RNFSLibraryDirectoryPath": _libraryDir, + @"RNFSPicturesDirectoryPath": [NSNull null], + @"RNFSFileTypeRegular": NSFileTypeRegular, + @"RNFSFileTypeDirectory": NSFileTypeDirectory, + @"RNFSFileProtectionComplete": NSFileProtectionComplete, + @"RNFSFileProtectionCompleteUnlessOpen": NSFileProtectionCompleteUnlessOpen, + @"RNFSFileProtectionCompleteUntilFirstUserAuthentication": NSFileProtectionCompleteUntilFirstUserAuthentication, + @"RNFSFileProtectionNone": NSFileProtectionNone, + }; +} + +#pragma mark - File operations + +RCT_EXPORT_METHOD(writeFile:(NSString *)filepath + contents:(NSString *)base64Content + options:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + NSData *data = [[NSData alloc] initWithBase64EncodedString:base64Content + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + BOOL success = [[NSFileManager defaultManager] createFileAtPath:path contents:data attributes:nil]; + if (!success) { + reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: could not write '%@'", path], nil); + return; + } + resolve(nil); +} + +RCT_EXPORT_METHOD(readFile:(NSString *)filepath + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file '%@'", path], nil); + return; + } + NSData *content = [[NSFileManager defaultManager] contentsAtPath:path]; + resolve([content base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]); +} + +RCT_EXPORT_METHOD(readDir:(NSString *)dirPath + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:dirPath reject:reject]; + if (!path) return; + + NSFileManager *fm = [NSFileManager defaultManager]; + NSError *error; + NSArray *contents = [fm contentsOfDirectoryAtPath:path error:&error]; + if (error) { + reject(@"ENOENT", error.localizedDescription, error); + return; + } + + NSMutableArray *result = [NSMutableArray new]; + for (NSString *name in contents) { + NSString *fullPath = [path stringByAppendingPathComponent:name]; + NSDictionary *attrs = [fm attributesOfItemAtPath:fullPath error:nil]; + if (attrs) { + [result addObject:@{ + @"ctime": @([(NSDate *)attrs[NSFileCreationDate] timeIntervalSince1970]), + @"mtime": @([(NSDate *)attrs[NSFileModificationDate] timeIntervalSince1970]), + @"name": name, + @"path": fullPath, + @"size": attrs[NSFileSize], + @"type": attrs[NSFileType], + }]; + } + } + resolve(result); +} + +RCT_EXPORT_METHOD(exists:(NSString *)filepath + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + resolve(@([[NSFileManager defaultManager] fileExistsAtPath:path])); +} + +RCT_EXPORT_METHOD(stat:(NSString *)filepath + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + NSError *error; + NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&error]; + if (error) { + reject(@"ENOENT", error.localizedDescription, error); + return; + } + + resolve(@{ + @"ctime": @([(NSDate *)attrs[NSFileCreationDate] timeIntervalSince1970]), + @"mtime": @([(NSDate *)attrs[NSFileModificationDate] timeIntervalSince1970]), + @"size": attrs[NSFileSize], + @"type": attrs[NSFileType], + @"mode": @([[(NSNumber *)attrs[NSFilePosixPermissions] stringValue] integerValue]), + }); +} + +RCT_EXPORT_METHOD(unlink:(NSString *)filepath + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + NSError *error; + if (![[NSFileManager defaultManager] removeItemAtPath:path error:&error]) { + reject(@"ENOENT", error.localizedDescription, error); + return; + } + resolve(nil); +} + +RCT_EXPORT_METHOD(mkdir:(NSString *)filepath + options:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + NSError *error; + if (![[NSFileManager defaultManager] createDirectoryAtPath:path + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + reject(@"ENOENT", error.localizedDescription, error); + return; + } + resolve(nil); +} + +RCT_EXPORT_METHOD(moveFile:(NSString *)filepath + destPath:(NSString *)destPath + options:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *src = [self _sandboxedPath:filepath reject:reject]; + if (!src) return; + NSString *dst = [self _sandboxedPath:destPath reject:reject]; + if (!dst) return; + + NSError *error; + if (![[NSFileManager defaultManager] moveItemAtPath:src toPath:dst error:&error]) { + reject(@"ENOENT", error.localizedDescription, error); + return; + } + resolve(nil); +} + +RCT_EXPORT_METHOD(copyFile:(NSString *)filepath + destPath:(NSString *)destPath + options:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *src = [self _sandboxedPath:filepath reject:reject]; + if (!src) return; + NSString *dst = [self _sandboxedPath:destPath reject:reject]; + if (!dst) return; + + NSError *error; + if (![[NSFileManager defaultManager] copyItemAtPath:src toPath:dst error:&error]) { + reject(@"ENOENT", error.localizedDescription, error); + return; + } + resolve(nil); +} + +RCT_EXPORT_METHOD(appendFile:(NSString *)filepath + contents:(NSString *)base64Content + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + NSData *data = [[NSData alloc] initWithBase64EncodedString:base64Content + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSFileManager *fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:path]) { + if (![fm createFileAtPath:path contents:data attributes:nil]) { + reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: could not create '%@'", path], nil); + return; + } + resolve(nil); + return; + } + + @try { + NSFileHandle *fh = [NSFileHandle fileHandleForUpdatingAtPath:path]; + [fh seekToEndOfFile]; + [fh writeData:data]; + resolve(nil); + } @catch (NSException *e) { + reject(@"ENOENT", e.reason, nil); + } +} + +RCT_EXPORT_METHOD(write:(NSString *)filepath + contents:(NSString *)base64Content + position:(NSInteger)position + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + NSData *data = [[NSData alloc] initWithBase64EncodedString:base64Content + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSFileManager *fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:path]) { + if (![fm createFileAtPath:path contents:data attributes:nil]) { + reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: could not create '%@'", path], nil); + return; + } + resolve(nil); + return; + } + + @try { + NSFileHandle *fh = [NSFileHandle fileHandleForUpdatingAtPath:path]; + if (position >= 0) { + [fh seekToFileOffset:position]; + } else { + [fh seekToEndOfFile]; + } + [fh writeData:data]; + resolve(nil); + } @catch (NSException *e) { + reject(@"ENOENT", e.reason, nil); + } +} + +RCT_EXPORT_METHOD(read:(NSString *)filepath + length:(NSInteger)length + position:(NSInteger)position + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file '%@'", path], nil); + return; + } + + NSFileHandle *fh = [NSFileHandle fileHandleForReadingAtPath:path]; + if (!fh) { + reject(@"ENOENT", @"Could not open file for reading", nil); + return; + } + [fh seekToFileOffset:(unsigned long long)position]; + NSData *content = (length > 0) ? [fh readDataOfLength:length] : [fh readDataToEndOfFile]; + resolve([content base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]); +} + +RCT_EXPORT_METHOD(hash:(NSString *)filepath + algorithm:(NSString *)algorithm + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + reject(@"ENOENT", [NSString stringWithFormat:@"ENOENT: no such file '%@'", path], nil); + return; + } + + NSData *content = [[NSFileManager defaultManager] contentsAtPath:path]; + + NSDictionary *digestLengths = @{ + @"md5": @(CC_MD5_DIGEST_LENGTH), + @"sha1": @(CC_SHA1_DIGEST_LENGTH), + @"sha224": @(CC_SHA224_DIGEST_LENGTH), + @"sha256": @(CC_SHA256_DIGEST_LENGTH), + @"sha384": @(CC_SHA384_DIGEST_LENGTH), + @"sha512": @(CC_SHA512_DIGEST_LENGTH), + }; + + int digestLength = [digestLengths[algorithm] intValue]; + if (!digestLength) { + reject(@"Error", [NSString stringWithFormat:@"Invalid hash algorithm '%@'", algorithm], nil); + return; + } + + unsigned char buffer[CC_SHA512_DIGEST_LENGTH]; + if ([algorithm isEqualToString:@"md5"]) CC_MD5(content.bytes, (CC_LONG)content.length, buffer); + else if ([algorithm isEqualToString:@"sha1"]) CC_SHA1(content.bytes, (CC_LONG)content.length, buffer); + else if ([algorithm isEqualToString:@"sha224"]) CC_SHA224(content.bytes, (CC_LONG)content.length, buffer); + else if ([algorithm isEqualToString:@"sha256"]) CC_SHA256(content.bytes, (CC_LONG)content.length, buffer); + else if ([algorithm isEqualToString:@"sha384"]) CC_SHA384(content.bytes, (CC_LONG)content.length, buffer); + else if ([algorithm isEqualToString:@"sha512"]) CC_SHA512(content.bytes, (CC_LONG)content.length, buffer); + + NSMutableString *output = [NSMutableString stringWithCapacity:digestLength * 2]; + for (int i = 0; i < digestLength; i++) { + [output appendFormat:@"%02x", buffer[i]]; + } + resolve(output); +} + +RCT_EXPORT_METHOD(getFSInfo:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSError *error; + NSDictionary *attrs = [[NSFileManager defaultManager] + attributesOfFileSystemForPath:_sandboxRoot error:&error]; + if (error) { + reject(@"Error", error.localizedDescription, error); + return; + } + resolve(@{ + @"totalSpace": attrs[NSFileSystemSize], + @"freeSpace": attrs[NSFileSystemFreeSize], + }); +} + +RCT_EXPORT_METHOD(touch:(NSString *)filepath + mtime:(NSDate *)mtime + ctime:(NSDate *)ctime + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *path = [self _sandboxedPath:filepath reject:reject]; + if (!path) return; + + NSMutableDictionary *attr = [NSMutableDictionary new]; + if (mtime) attr[NSFileModificationDate] = mtime; + if (ctime) attr[NSFileCreationDate] = ctime; + + NSError *error; + if (![[NSFileManager defaultManager] setAttributes:attr ofItemAtPath:path error:&error]) { + reject(@"ENOENT", error.localizedDescription, error); + return; + } + resolve(nil); +} + +#pragma mark - Stubbed network operations (blocked in sandbox) + +RCT_EXPORT_METHOD(downloadFile:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"downloadFile is not available in sandboxed mode", nil); +} + +RCT_EXPORT_METHOD(stopDownload:(nonnull NSNumber *)jobId) +{ + RCTLogWarn(@"[SandboxedRNFSManager] stopDownload blocked in sandbox"); +} + +RCT_EXPORT_METHOD(resumeDownload:(nonnull NSNumber *)jobId) +{ + RCTLogWarn(@"[SandboxedRNFSManager] resumeDownload blocked in sandbox"); +} + +RCT_EXPORT_METHOD(isResumable:(nonnull NSNumber *)jobId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + resolve(@NO); +} + +RCT_EXPORT_METHOD(uploadFiles:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"uploadFiles is not available in sandboxed mode", nil); +} + +RCT_EXPORT_METHOD(stopUpload:(nonnull NSNumber *)jobId) +{ + RCTLogWarn(@"[SandboxedRNFSManager] stopUpload blocked in sandbox"); +} + +RCT_EXPORT_METHOD(completeHandlerIOS:(nonnull NSNumber *)jobId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + resolve(nil); +} + +RCT_EXPORT_METHOD(pathForBundle:(NSString *)bundleNamed + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"pathForBundle is not available in sandboxed mode", nil); +} + +RCT_EXPORT_METHOD(pathForGroup:(NSString *)groupId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + reject(@"EPERM", @"pathForGroup is not available in sandboxed mode", nil); +} + +// addListener / removeListeners required by RCTEventEmitter +RCT_EXPORT_METHOD(addListener:(NSString *)eventName) {} +RCT_EXPORT_METHOD(removeListeners:(double)count) {} + +#pragma mark - RCTTurboModule + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +@end diff --git a/apps/fs-experiment/jest.harness.config.mjs b/apps/fs-experiment/jest.harness.config.mjs new file mode 100644 index 0000000..3e6740f --- /dev/null +++ b/apps/fs-experiment/jest.harness.config.mjs @@ -0,0 +1,4 @@ +export default { + preset: 'react-native-harness', + testMatch: ['**/__tests__/**/*.harness.{ts,tsx}'], +} diff --git a/apps/fs-experiment/metro.config.js b/apps/fs-experiment/metro.config.js index 28a5f80..5bd7612 100644 --- a/apps/fs-experiment/metro.config.js +++ b/apps/fs-experiment/metro.config.js @@ -1,9 +1,35 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config') const path = require('path') +const fs = require('fs') const projectRoot = __dirname const workspaceRoot = path.resolve(projectRoot, '../..') +const appNodeModules = path.resolve(projectRoot, 'node_modules') +const rootNodeModules = path.resolve(workspaceRoot, 'node_modules') + +// Pin react-native and all @react-native/* scoped packages so that both the +// main bundle and the harness test bundle resolve the same 0.82.1 version +// instead of accidentally picking up the monorepo root's 0.80.1 copy. +const extraNodeModules = {} + +function pinRnScoped(dir) { + const scopeDir = path.join(dir, '@react-native') + if (fs.existsSync(scopeDir)) { + for (const entry of fs.readdirSync(scopeDir)) { + const key = `@react-native/${entry}` + if (!extraNodeModules[key]) { + extraNodeModules[key] = path.join(scopeDir, entry) + } + } + } +} +pinRnScoped(appNodeModules) +pinRnScoped(rootNodeModules) + +extraNodeModules['react-native'] = path.join(appNodeModules, 'react-native') +extraNodeModules['react'] = path.join(appNodeModules, 'react') + /** * Metro configuration * https://reactnative.dev/docs/metro @@ -13,17 +39,12 @@ const workspaceRoot = path.resolve(projectRoot, '../..') const config = { watchFolders: [workspaceRoot], resolver: { - nodeModulesPaths: [ - path.resolve(projectRoot, 'node_modules'), - path.resolve(workspaceRoot, 'node_modules'), - ], + nodeModulesPaths: [appNodeModules, rootNodeModules], + extraNodeModules, disableHierarchicalLookup: true, + unstable_enablePackageExports: true, }, - serializer: { - // Enable multiple entry points for sandbox bundles - getModulesRunBeforeMainModule: () => [], - }, - // Configure entry points for sandbox bundles + serializer: {}, transformer: { getTransformOptions: async () => ({ transform: { diff --git a/apps/fs-experiment/package.json b/apps/fs-experiment/package.json index 384693a..ebd095a 100644 --- a/apps/fs-experiment/package.json +++ b/apps/fs-experiment/package.json @@ -4,30 +4,41 @@ "private": true, "scripts": { "android": "react-native run-android", - "ios": "react-native run-ios", + "ios": "react-native run-ios", "start": "react-native start", - "bundle:sandbox": "bun run bundle:sandbox-fs && bun run bundle:sandbox-file-access", - "bundle:sandbox-fs": "npx react-native bundle --platform ios --dev false --entry-file sandbox-fs.js --bundle-output ios/sandbox-fs.jsbundle --assets-dest ios/", - "bundle:sandbox-file-access": "npx react-native bundle --platform ios --dev false --entry-file sandbox-file-access.js --bundle-output ios/sandbox-file-access.jsbundle --assets-dest ios/", + "bundle:sandbox": "npx react-native bundle --platform ios --dev false --entry-file sandbox.js --bundle-output ios/sandbox.jsbundle --assets-dest ios/", "typecheck": "tsc --noEmit", - "jest": "echo 'No tests'" + "jest": "echo 'No tests'", + "harness": "react-native-harness --harnessRunner ios && react-native-harness --harnessRunner android", + "harness:ios": "react-native-harness --harnessRunner ios", + "harness:android": "react-native-harness --harnessRunner android" }, "dependencies": { - "react": "19.1.0", - "react-native": "0.80.1", "@callstack/react-native-sandbox": "workspace:*", - "react-native-fs": "^2.20.0", - "react-native-file-access": "^3.1.1" + "@react-native-async-storage/async-storage": "^2.1.2", + "@react-native/assets-registry": "0.82.1", + "@react-native/js-polyfills": "0.82.1", + "@react-native/normalize-colors": "0.82.1", + "@react-native/virtualized-lists": "0.82.1", + "event-target-shim": "^6.0.2", + "react": "19.1.1", + "react-native": "0.82.1", + "react-native-file-access": "^3.1.1", + "react-native-fs": "^2.20.0" }, "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", - "@react-native/babel-preset": "0.80.1", - "@react-native/eslint-config": "0.80.1", - "@react-native/metro-config": "0.80.1", - "@react-native/typescript-config": "0.80.1", - "react-test-renderer": "19.1.0" + "@react-native-harness/platform-android": "^1.0.0", + "@react-native-harness/platform-apple": "^1.0.0", + "@react-native-harness/ui": "^1.0.0", + "@react-native/babel-preset": "0.82.1", + "@react-native/eslint-config": "0.82.1", + "@react-native/metro-config": "0.82.1", + "@react-native/typescript-config": "0.82.1", + "react-native-harness": "^1.0.0", + "react-test-renderer": "19.1.1" }, "engines": { "node": ">=18" diff --git a/apps/fs-experiment/rn-harness.config.mjs b/apps/fs-experiment/rn-harness.config.mjs new file mode 100644 index 0000000..240080c --- /dev/null +++ b/apps/fs-experiment/rn-harness.config.mjs @@ -0,0 +1,38 @@ +import { + androidEmulator, + androidPlatform, +} from '@react-native-harness/platform-android' +import { + applePlatform, + appleSimulator, +} from '@react-native-harness/platform-apple' + +const iosDevice = process.env.HARNESS_IOS_DEVICE ?? 'iPhone 16 Pro' +const iosVersion = process.env.HARNESS_IOS_VERSION ?? '18.4' +const androidDevice = + process.env.HARNESS_ANDROID_DEVICE ?? 'Medium_Phone_API_35' + +const config = { + entryPoint: './index.js', + appRegistryComponentName: 'MultInstance-FSExperiment', + + runners: [ + applePlatform({ + name: 'ios', + device: appleSimulator(iosDevice, iosVersion), + bundleId: 'org.reactjs.native.example.MultInstance-FSExperiment', + }), + androidPlatform({ + name: 'android', + device: androidEmulator(androidDevice), + bundleId: 'com.multinstance.fsexperiment', + }), + ], + + defaultRunner: 'ios', + forwardClientLogs: true, + resetEnvironmentBetweenTestFiles: true, + disableViewFlattening: true, +} + +export default config diff --git a/apps/fs-experiment/sandbox-file-access.js b/apps/fs-experiment/sandbox-file-access.js deleted file mode 100644 index 1fca809..0000000 --- a/apps/fs-experiment/sandbox-file-access.js +++ /dev/null @@ -1,5 +0,0 @@ -import {AppRegistry} from 'react-native' - -import SandboxFileAccess from './SandboxFileAccess' - -AppRegistry.registerComponent('AppFileAccess', () => SandboxFileAccess) diff --git a/apps/fs-experiment/sandbox-fs.js b/apps/fs-experiment/sandbox-fs.js deleted file mode 100644 index 12eec66..0000000 --- a/apps/fs-experiment/sandbox-fs.js +++ /dev/null @@ -1,5 +0,0 @@ -import {AppRegistry} from 'react-native' - -import SandboxFS from './SandboxFS' - -AppRegistry.registerComponent('AppFS', () => SandboxFS) diff --git a/apps/fs-experiment/sandbox.js b/apps/fs-experiment/sandbox.js new file mode 100644 index 0000000..e0bc3a3 --- /dev/null +++ b/apps/fs-experiment/sandbox.js @@ -0,0 +1,66 @@ +import AsyncStorage from '@react-native-async-storage/async-storage' +import React, {useEffect} from 'react' +import {AppRegistry} from 'react-native' +import {Dirs, FileSystem} from 'react-native-file-access' + +import FileOpsUI from './FileOpsUI' + +/** + * Handles test commands sent from the host via postMessage. + * This enables cross-boundary isolation tests: the host writes data, + * then asks the sandbox to read the same key/file to verify isolation. + * + * Commands: + * { cmd: 'write', module: 'file-access'|'async-storage', target, content } + * { cmd: 'read', module: 'file-access'|'async-storage', target } + * + * Responses sent back via postMessage: + * { cmd, ok: true, result: string } + * { cmd, ok: false, error: string } + */ +function installTestCommandHandler() { + if (typeof globalThis.setOnMessage !== 'function') return + + globalThis.setOnMessage(async msg => { + if (!msg || typeof msg !== 'object' || !msg.cmd) return + + const {cmd, module, target, content, id} = msg + + const reply = data => { + if (typeof globalThis.postMessage === 'function') { + globalThis.postMessage({...data, id}) + } + } + + try { + if (cmd === 'write') { + if (module === 'file-access') { + await FileSystem.writeFile(`${Dirs.DocumentDir}/${target}`, content) + } else if (module === 'async-storage') { + await AsyncStorage.setItem(target, content) + } + reply({cmd: 'write', ok: true, result: content}) + } else if (cmd === 'read') { + let result + if (module === 'file-access') { + result = await FileSystem.readFile(`${Dirs.DocumentDir}/${target}`) + } else if (module === 'async-storage') { + result = (await AsyncStorage.getItem(target)) ?? null + } + reply({cmd: 'read', ok: true, result}) + } + } catch (e) { + reply({cmd, ok: false, error: e.message}) + } + }) +} + +function SandboxApp(props) { + useEffect(() => { + installTestCommandHandler() + }, []) + + return +} + +AppRegistry.registerComponent('SandboxApp', () => SandboxApp) diff --git a/bun.lock b/bun.lock index 202ced1..7950b06 100644 --- a/bun.lock +++ b/bun.lock @@ -67,8 +67,14 @@ "version": "1.0.0", "dependencies": { "@callstack/react-native-sandbox": "workspace:*", - "react": "19.1.0", - "react-native": "0.80.1", + "@react-native-async-storage/async-storage": "^2.1.2", + "@react-native/assets-registry": "0.82.1", + "@react-native/js-polyfills": "0.82.1", + "@react-native/normalize-colors": "0.82.1", + "@react-native/virtualized-lists": "0.82.1", + "event-target-shim": "^6.0.2", + "react": "19.1.1", + "react-native": "0.82.1", "react-native-file-access": "^3.1.1", "react-native-fs": "^2.20.0", }, @@ -76,11 +82,15 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", - "@react-native/babel-preset": "0.80.1", - "@react-native/eslint-config": "0.80.1", - "@react-native/metro-config": "0.80.1", - "@react-native/typescript-config": "0.80.1", - "react-test-renderer": "19.1.0", + "@react-native-harness/platform-android": "^1.0.0", + "@react-native-harness/platform-apple": "^1.0.0", + "@react-native-harness/ui": "^1.0.0", + "@react-native/babel-preset": "0.82.1", + "@react-native/eslint-config": "0.82.1", + "@react-native/metro-config": "0.82.1", + "@react-native/typescript-config": "0.82.1", + "react-native-harness": "^1.0.0", + "react-test-renderer": "19.1.1", }, }, "apps/p2p-chat": { @@ -178,7 +188,7 @@ }, "packages/react-native-sandbox": { "name": "@callstack/react-native-sandbox", - "version": "0.4.1", + "version": "0.5.0", "devDependencies": { "react": "19.1.0", "react-native": "0.80.1", @@ -462,6 +472,10 @@ "@callstack/repack-dev-server": ["@callstack/repack-dev-server@5.1.3", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@fastify/middie": "^8.3.0", "@fastify/sensible": "^5.5.0", "@react-native/dev-middleware": "^0.78.0", "fastify": "^4.24.3", "fastify-favicon": "^4.3.0", "fastify-plugin": "^4.5.1", "http-proxy-middleware": "^3.0.3", "launch-editor": "^2.10.0", "open": "^10.1.0", "pretty-format": "^28.1.0", "source-map": "^0.7.4", "ws": "^8.18.1" } }, "sha512-dj2RA8c3SUme3MO/g2K7hA3LsOCQdK44vht/1i5PnSBFTTkSkHDuEa6KyaQ1uId/XWtye2G/GZo1a10jXZTcbw=="], + "@clack/core": ["@clack/core@1.0.0-alpha.7", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-3vdh6Ar09D14rVxJZIm3VQJkU+ZOKKT5I5cC0cOVazy70CNyYYjiwRj9unwalhESndgxx6bGc/m6Hhs4EKF5XQ=="], + + "@clack/prompts": ["@clack/prompts@1.0.0-alpha.9", "", { "dependencies": { "@clack/core": "1.0.0-alpha.7", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-sKs0UjiHFWvry4SiRfBi5Qnj0C/6AYx8aKkFPZQSuUZXgAram25ZDmhQmP7vj1aFyLpfHWtLQjWvOvcat0TOLg=="], + "@commitlint/cli": ["@commitlint/cli@17.8.1", "", { "dependencies": { "@commitlint/format": "^17.8.1", "@commitlint/lint": "^17.8.1", "@commitlint/load": "^17.8.1", "@commitlint/read": "^17.8.1", "@commitlint/types": "^17.8.1", "execa": "^5.0.0", "lodash.isfunction": "^3.0.9", "resolve-from": "5.0.0", "resolve-global": "1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "cli.js" } }, "sha512-ay+WbzQesE0Rv4EQKfNbSMiJJ12KdKTDzIt0tcK4k11FdsWmtwP0Kp1NWMOUswfIWo6Eb7p7Ln721Nx9FLNBjg=="], "@commitlint/config-conventional": ["@commitlint/config-conventional@17.8.1", "", { "dependencies": { "conventional-changelog-conventionalcommits": "^6.1.0" } }, "sha512-NxCOHx1kgneig3VLauWJcDWS40DVjg7nKOpBEEK9E5fjJpQqLCilcnKkIIjdBH98kEO1q3NpE5NSrZ2kl/QGJg=="], @@ -680,6 +694,8 @@ "@pnpm/npm-conf": ["@pnpm/npm-conf@2.3.1", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw=="], + "@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.2.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw=="], + "@react-native-community/cli": ["@react-native-community/cli@18.0.0", "", { "dependencies": { "@react-native-community/cli-clean": "18.0.0", "@react-native-community/cli-config": "18.0.0", "@react-native-community/cli-doctor": "18.0.0", "@react-native-community/cli-server-api": "18.0.0", "@react-native-community/cli-tools": "18.0.0", "@react-native-community/cli-types": "18.0.0", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-DyKptlG78XPFo7tDod+we5a3R+U9qjyhaVFbOPvH4pFNu5Dehewtol/srl44K6Cszq0aEMlAJZ3juk0W4WnOJA=="], "@react-native-community/cli-clean": ["@react-native-community/cli-clean@18.0.0", "", { "dependencies": { "@react-native-community/cli-tools": "18.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-+k64EnJaMI5U8iNDF9AftHBJW+pO/isAhncEXuKRc6IjRtIh6yoaUIIf5+C98fgjfux7CNRZAMQIkPbZodv2Gw=="], @@ -704,7 +720,33 @@ "@react-native-community/cli-types": ["@react-native-community/cli-types@18.0.0", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-J84+4IRXl8WlVdoe1maTD5skYZZO9CbQ6LNXEHx1kaZcFmvPZKfjsaxuyQ+8BsSqZnM2izOw8dEWnAp/Zuwb0w=="], - "@react-native/assets-registry": ["@react-native/assets-registry@0.80.1", "", {}, "sha512-T3C8OthBHfpFIjaGFa0q6rc58T2AsJ+jKAa+qPquMKBtYGJMc75WgNbk/ZbPBxeity6FxZsmg3bzoUaWQo4Mow=="], + "@react-native-harness/babel-preset": ["@react-native-harness/babel-preset@1.0.0", "", { "dependencies": { "@babel/plugin-transform-class-static-block": "^7.27.1", "babel-plugin-istanbul": "^7.0.1" }, "peerDependencies": { "@babel/core": "^7.22.0", "@babel/plugin-transform-react-jsx": "*" } }, "sha512-YaTLNxEuJitzEea1v/rIabIXPc+BpXBBo1AosMor/vlR5C4PdIcAXRLzyEL9I2lDdPK+qHInG9KWpvohk9uW6A=="], + + "@react-native-harness/bridge": ["@react-native-harness/bridge@1.0.0", "", { "dependencies": { "@react-native-harness/platforms": "1.0.0", "@react-native-harness/tools": "1.0.0", "birpc": "^2.4.0", "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", "ssim.js": "^3.5.0", "tslib": "^2.3.0", "ws": "^8.18.2" } }, "sha512-S3tPNumuFO98aJWtVMB01O56EXXhDedYTbMBWBaE6fTn7YftK6KCgGDMDPL1SzsXNQrOIO4ypti6IwP63QWlpA=="], + + "@react-native-harness/bundler-metro": ["@react-native-harness/bundler-metro@1.0.0", "", { "dependencies": { "@react-native-harness/config": "1.0.0", "@react-native-harness/metro": "1.0.0", "@react-native-harness/tools": "1.0.0", "connect": "^3.7.0", "nocache": "^4.0.0", "tslib": "^2.3.0" }, "peerDependencies": { "metro": "*", "metro-config": "*" } }, "sha512-Aj+hA8MY4zfnrijSOXRU3AL0cjPOX+tV6q/kSrHUw7E07KxSQ7xSSqmmhjtLrjU8OvJaoYu3e2Vprf38E61i1Q=="], + + "@react-native-harness/cli": ["@react-native-harness/cli@1.0.0", "", { "dependencies": { "@react-native-harness/bridge": "1.0.0", "@react-native-harness/config": "1.0.0", "@react-native-harness/platforms": "1.0.0", "@react-native-harness/tools": "1.0.0", "tslib": "^2.3.0" }, "peerDependencies": { "jest-cli": "*" } }, "sha512-EgmYIRGXdREIdKQiaBnoDqFvlUtvnevslsLedjBZawxPmMVFv5ABRdNbYUvSG5f6k6waK70bAyuTjoQ7oCBrew=="], + + "@react-native-harness/config": ["@react-native-harness/config@1.0.0", "", { "dependencies": { "@react-native-harness/tools": "1.0.0", "tslib": "^2.3.0", "zod": "^3.25.67" } }, "sha512-xLGs/b4pzUW2MjmD0J7SaE1V87CSEXtH6HfPjOmilbgnRFBz/Bg69GajkpMfLRCuH5d31NXnJQFfhX3KjkwmZA=="], + + "@react-native-harness/jest": ["@react-native-harness/jest@1.0.0", "", { "dependencies": { "@jest/test-result": "^30.2.0", "@react-native-harness/bridge": "1.0.0", "@react-native-harness/bundler-metro": "1.0.0", "@react-native-harness/config": "1.0.0", "@react-native-harness/platforms": "1.0.0", "@react-native-harness/tools": "1.0.0", "chalk": "^4.1.2", "jest-message-util": "^30.2.0", "jest-util": "^30.2.0", "p-limit": "^7.1.1", "tslib": "^2.3.0", "yargs": "^17.7.2" } }, "sha512-AsDguF6RAM//Wygnf4BlX4nzZybzfk7TnXELTwreM4FYgRHPwXLbbQZC9BIfjKabj8cvnxOoGXiGZ9hU9jHcmw=="], + + "@react-native-harness/metro": ["@react-native-harness/metro@1.0.0", "", { "dependencies": { "@react-native-harness/babel-preset": "1.0.0", "@react-native-harness/config": "1.0.0", "tslib": "^2.3.0" }, "peerDependencies": { "@react-native-harness/runtime": "1.0.0", "metro": "*" } }, "sha512-+J3GtdKKOAMtseV9YL0WRzSVbS2Y+wck3n9OoBErH9iBlFev22FtalUamG5KWWy/y4jRfQjWtYK+B91UoZlUGg=="], + + "@react-native-harness/platform-android": ["@react-native-harness/platform-android@1.0.0", "", { "dependencies": { "@react-native-harness/config": "1.0.0", "@react-native-harness/platforms": "1.0.0", "@react-native-harness/tools": "1.0.0", "tslib": "^2.3.0", "zod": "^3.25.67" } }, "sha512-tIZVdFwtRoPp8XN9drINxbxqmPti6j+ULbf73d/UYTAGdP0AXxzziKIFMWhWYut6jw0EFF0tY9DxlIifgpOLRQ=="], + + "@react-native-harness/platform-apple": ["@react-native-harness/platform-apple@1.0.0", "", { "dependencies": { "@react-native-harness/platforms": "1.0.0", "@react-native-harness/tools": "1.0.0", "tslib": "^2.3.0", "zod": "^3.25.67" } }, "sha512-TKfraoHrrgLu2KzU+iJjKvZlU95Gt20P7nhb9GHbs5iGxJ+bzFWsFq4m0LV6BHpspIx8NprHU59vGMQkqjzg1w=="], + + "@react-native-harness/platforms": ["@react-native-harness/platforms@1.0.0", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-z3V4J/TkhGYL239nOiqFuW+A7bNlINc4EtVrkp2rpHChJf7JXH1chzY5rVuIDLVxo8/MsGrF7AkJRgK1HsVrQg=="], + + "@react-native-harness/runtime": ["@react-native-harness/runtime@1.0.0", "", { "dependencies": { "@react-native-harness/bridge": "1.0.0", "@vitest/expect": "4.0.16", "@vitest/spy": "4.0.16", "chai": "^6.2.2", "event-target-shim": "^6.0.2", "use-sync-external-store": "^1.6.0", "zustand": "^5.0.5" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-3KFkaHoskIl35U/12O3Ut0MwvU/LXNfsnJuKxiJGOXFEuvuCgNqc0Cm+NXNvEvCkh2nXZJjvsRBh8Icv0gt/Fw=="], + + "@react-native-harness/tools": ["@react-native-harness/tools@1.0.0", "", { "dependencies": { "@clack/prompts": "1.0.0-alpha.9", "is-unicode-supported": "^0.1.0", "nano-spawn": "^1.0.2", "picocolors": "^1.1.1", "tslib": "^2.3.0" }, "peerDependencies": { "react-native": "*" } }, "sha512-xesxIQkTAc69Of2mozXC/wnzhzBQSkOvx7CEidRMD8jok1RfkYuj5JbwWHgBh8vaScixsqP2WDuBF2MiLwOuCw=="], + + "@react-native-harness/ui": ["@react-native-harness/ui@1.0.0", "", { "dependencies": { "@react-native-harness/runtime": "1.0.0", "tslib": "^2.3.0" }, "peerDependencies": { "react-native": "*" } }, "sha512-jVrcALCZCuEiw9tIDQsARHac9sxTk7Db6FJmVjhWAeZGhGyBPrJMfpogyEElZ0VeVS1imZ3ryIWU0PN/7OKhfw=="], + + "@react-native/assets-registry": ["@react-native/assets-registry@0.82.1", "", {}, "sha512-B1SRwpntaAcckiatxbjzylvNK562Ayza05gdJCjDQHTiDafa1OABmyB5LHt7qWDOpNkaluD+w11vHF7pBmTpzQ=="], "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.80.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.80.1" } }, "sha512-A0xTmTiHamvKL2AtkUQqT+5WcLMFLig/62STT5Aa/CyxfqpkmSyKbHwSUEiMZ744SARG8JEG+D++dgy6steqag=="], @@ -716,6 +758,8 @@ "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.80.1", "", {}, "sha512-5dQJdX1ZS4dINNw51KNsDIL+A06sZQd2hqN2Pldq5SavxAwEJh5NxAx7K+lutKhwp1By5gxd6/9ruVt+9NCvKA=="], + "@react-native/debugger-shell": ["@react-native/debugger-shell@0.82.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "fb-dotslash": "0.5.8" } }, "sha512-fdRHAeqqPT93bSrxfX+JHPpCXHApfDUdrXMXhoxlPgSzgXQXJDykIViKhtpu0M6slX6xU/+duq+AtP/qWJRpBw=="], + "@react-native/dev-middleware": ["@react-native/dev-middleware@0.80.1", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.80.1", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-EBnZ3s6+hGAlUggDvo9uI37Xh0vG55H2rr3A6l6ww7+sgNuUz+wEJ63mGINiU6DwzQSgr6av7rjrVERxKH6vxg=="], "@react-native/eslint-config": ["@react-native/eslint-config@0.80.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/eslint-parser": "^7.25.1", "@react-native/eslint-plugin": "0.80.1", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-ft-flow": "^2.0.1", "eslint-plugin-jest": "^27.9.0", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-native": "^4.0.0" }, "peerDependencies": { "eslint": ">=8", "prettier": ">=2" } }, "sha512-bGtMGDfLDLZ5WWIo+OjfnDo4s1TaKfD0W4OoqeqmIcAkbVYBvTLGcjBW4MsMDO5JVebErUMGa+vE0z7p3B0Nsg=="], @@ -724,7 +768,7 @@ "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.80.1", "", {}, "sha512-6B7bWUk27ne/g/wCgFF4MZFi5iy6hWOcBffqETJoab6WURMyZ6nU+EAMn+Vjhl5ishhUvTVSrJ/1uqrxxYQO2Q=="], - "@react-native/js-polyfills": ["@react-native/js-polyfills@0.80.1", "", {}, "sha512-cWd5Cd2kBMRM37dor8N9Ck4X0NzjYM3m8K6HtjodcOdOvzpXfrfhhM56jdseTl5Z4iB+pohzPJpSmFJctmuIpA=="], + "@react-native/js-polyfills": ["@react-native/js-polyfills@0.82.1", "", {}, "sha512-tf70X7pUodslOBdLN37J57JmDPB/yiZcNDzS2m+4bbQzo8fhx3eG9QEBv5n4fmzqfGAgSB4BWRHgDMXmmlDSVA=="], "@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.80.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.80.1", "hermes-parser": "0.28.1", "nullthrows": "^1.1.1" } }, "sha512-pHKwMfoSoScWHmYAd8xSa2wyMq0GESibP1/yeFjtkqaMQKNuEcfKdrXrTrrmMje6psaB17UizvHBevsnEUWWFA=="], @@ -732,11 +776,11 @@ "@react-native/new-app-screen": ["@react-native/new-app-screen@0.80.2", "", { "peerDependencies": { "@types/react": "^19.0.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-cszaO1s1WJNBSkvb9bYGxv9CYcHDbGMMmtgemhpe1HyzcoE2kKp9FLn6aAUxIP+aZ2RBP1Zya1Wt8oRuCyTNjQ=="], - "@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="], + "@react-native/normalize-colors": ["@react-native/normalize-colors@0.82.1", "", {}, "sha512-CCfTR1uX+Z7zJTdt3DNX9LUXr2zWXsNOyLbwupW2wmRzrxlHRYfmLgTABzRL/cKhh0Ubuwn15o72MQChvCRaHw=="], "@react-native/typescript-config": ["@react-native/typescript-config@0.80.1", "", {}, "sha512-rcDV6BMh+rjsYIh/f0pd6mWOKN4eTS9nov7CyeU2cAfuIEslScEJ7Kv6YZtlLZ5PgC9teUf5L9pqzx9GwmzY/w=="], - "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.80.1", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.0.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-nqQAeHheSNZBV+syhLVMgKBZv+FhCANfxAWVvfEXZa4rm5jGHsj3yA9vqrh2lcJL3pjd7PW5nMX7TcuJThEAgQ=="], + "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.82.1", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-f5zpJg9gzh7JtCbsIwV+4kP3eI0QBuA93JGmwFRd4onQ3DnCjV2J5pYqdWtM95sjSKK1dyik59Gj01lLeKqs1Q=="], "@release-it-plugins/workspaces": ["@release-it-plugins/workspaces@4.2.1", "", { "dependencies": { "detect-indent": "^6.0.0", "detect-newline": "^3.1.0", "semver": "^7.1.3", "url-join": "^4.0.1", "validate-peer-dependencies": "^1.0.0", "walk-sync": "^2.0.2", "yaml": "^2.1.1" }, "peerDependencies": { "release-it": "^14.0.0 || ^15.2.0 || ^16.0.0 || ^17.0.0" } }, "sha512-lZEARr5tqsFskTPHBLjmYxqfC/DJkWRc0Q/wjOODHW8rtYPhL1w2zv7fwvgSjhFmMp0OdsJOXzkcSkUDvc9NnA=="], @@ -786,6 +830,8 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], @@ -808,6 +854,10 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], @@ -916,6 +966,14 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="], + + "@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="], + + "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], + "@vscode/sudo-prompt": ["@vscode/sudo-prompt@9.3.1", "", {}, "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA=="], "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], @@ -1024,6 +1082,8 @@ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], "astral-regex": ["astral-regex@1.0.0", "", {}, "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="], @@ -1074,6 +1134,8 @@ "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], + "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], @@ -1116,6 +1178,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001731", "", {}, "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], @@ -1412,7 +1476,7 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "event-target-shim": ["event-target-shim@6.0.2", "", {}, "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA=="], "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -1462,6 +1526,8 @@ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="], + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], "fbjs": ["fbjs@3.0.5", "", { "dependencies": { "cross-fetch": "^3.1.5", "fbjs-css-vars": "^1.0.0", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", "setimmediate": "^1.0.5", "ua-parser-js": "^1.0.35" } }, "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg=="], @@ -1582,6 +1648,8 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hermes-compiler": ["hermes-compiler@0.0.0", "", {}, "sha512-boVFutx6ME/Km2mB6vvsQcdnazEYYI/jV1pomx1wcFUG/EVqTkr5CU0CW9bKipOA/8Hyu3NYwW3THg2Q1kNCfA=="], + "hermes-estree": ["hermes-estree@0.28.1", "", {}, "sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ=="], "hermes-parser": ["hermes-parser@0.28.1", "", { "dependencies": { "hermes-estree": "0.28.1" } }, "sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg=="], @@ -1710,7 +1778,7 @@ "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], - "is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + "is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], @@ -1962,6 +2030,8 @@ "meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="], + "merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], @@ -2020,6 +2090,8 @@ "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], + "nano-spawn": ["nano-spawn@1.0.3", "", {}, "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA=="], + "napi-postinstall": ["napi-postinstall@0.3.2", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -2094,7 +2166,7 @@ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -2146,8 +2218,12 @@ "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + "pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="], + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], @@ -2220,6 +2296,8 @@ "react-native-fs": ["react-native-fs@2.20.0", "", { "dependencies": { "base-64": "^0.1.0", "utf8": "^3.0.0" }, "peerDependencies": { "react-native": "*", "react-native-windows": "*" }, "optionalPeers": ["react-native-windows"] }, "sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ=="], + "react-native-harness": ["react-native-harness@1.0.0", "", { "dependencies": { "@react-native-harness/babel-preset": "1.0.0", "@react-native-harness/cli": "1.0.0", "@react-native-harness/jest": "1.0.0", "@react-native-harness/metro": "1.0.0", "@react-native-harness/runtime": "1.0.0", "tslib": "^2.3.0" }, "bin": { "harness": "bin.js", "react-native-harness": "bin.js" } }, "sha512-7D0luL7D5yDrV1ehQo9DOIwu8I3Ft1udfvdT5uepFxGBZZpF0c1aLr1zLTVLtg3/YKlJp2/RtJJLwlDcxiaALg=="], + "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.1.7", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w=="], "react-native-reanimated": ["react-native-reanimated@3.19.0", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-FNfqLuPuVHsW9KcsZtnJqIPlMQvuySnSFJXgSt9fVDPqptbSUkiAF6MthUwd4Mxt05hCRcbV+T65CENgVS5iCg=="], @@ -2406,6 +2484,8 @@ "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "ssim.js": ["ssim.js@3.5.0", "", {}, "sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g=="], + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], @@ -2488,6 +2568,8 @@ "tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], @@ -2570,6 +2652,8 @@ "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "utf8": ["utf8@3.0.0", "", {}, "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -2656,10 +2740,28 @@ "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="], + + "@apps/fs-experiment/@react-native/babel-preset": ["@react-native/babel-preset@0.82.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.82.1", "babel-plugin-syntax-hermes-parser": "0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-Olj7p4XIsUWLKjlW46CqijaXt45PZT9Lbvv/Hz698FXTenPKk4k7sy6RGRGZPWO2TCBBfcb73dus1iNHRFSq7g=="], + + "@apps/fs-experiment/@react-native/eslint-config": ["@react-native/eslint-config@0.82.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/eslint-parser": "^7.25.1", "@react-native/eslint-plugin": "0.82.1", "@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/parser": "^8.36.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-ft-flow": "^2.0.1", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-native": "^4.0.0" }, "peerDependencies": { "eslint": ">=8", "prettier": ">=2" } }, "sha512-K3xCTEAg8WDd7WpDhQ1hsKbuY3OXaQtqpokeOdgyJag100ZvUX84YIaqDqsVaAZqjA53zCA5PbxerWs6mPA+PQ=="], + + "@apps/fs-experiment/@react-native/metro-config": ["@react-native/metro-config@0.82.1", "", { "dependencies": { "@react-native/js-polyfills": "0.82.1", "@react-native/metro-babel-transformer": "0.82.1", "metro-config": "^0.83.1", "metro-runtime": "^0.83.1" } }, "sha512-mAY6R3xnDMlmDOrUCAtLTjIkli26DZt4LNVuAjDEdnlv5sHANOr5x4qpMn7ea1p9Q/tpfHLalPQUQeJ8CZH4gA=="], + + "@apps/fs-experiment/@react-native/typescript-config": ["@react-native/typescript-config@0.82.1", "", {}, "sha512-kCTjmBg44p0kqU4xEMg7l6SNJyHWTHuTqiT9MpHasEYcnVpBWyEQsSQAiVKONHwcUWcAktrGVLE1dYGfBmPJ3Q=="], + + "@apps/fs-experiment/react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="], + + "@apps/fs-experiment/react-native": ["react-native@0.82.1", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.82.1", "@react-native/codegen": "0.82.1", "@react-native/community-cli-plugin": "0.82.1", "@react-native/gradle-plugin": "0.82.1", "@react-native/js-polyfills": "0.82.1", "@react-native/normalize-colors": "0.82.1", "@react-native/virtualized-lists": "0.82.1", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "hermes-compiler": "0.0.0", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.1.1" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-tFAqcU7Z4g49xf/KnyCEzI4nRTu1Opcx05Ov2helr8ZTg1z7AJR/3sr2rZ+AAVlAs2IXk+B0WOxXGmdD3+4czA=="], + + "@apps/fs-experiment/react-test-renderer": ["react-test-renderer@19.1.1", "", { "dependencies": { "react-is": "^19.1.1", "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-aGRXI+zcBTtg0diHofc7+Vy97nomBs9WHHFY1Csl3iV0x6xucjNYZZAkiVKGiNYUv23ecOex5jE67t8ZzqYObA=="], + "@babel/eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@2.1.0", "", {}, "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="], "@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -2796,6 +2898,20 @@ "@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@react-native-harness/babel-preset/babel-plugin-istanbul": ["babel-plugin-istanbul@7.0.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-instrument": "^6.0.2", "test-exclude": "^6.0.0" } }, "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA=="], + + "@react-native-harness/bridge/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "@react-native-harness/bundler-metro/nocache": ["nocache@4.0.0", "", {}, "sha512-AntnTbmKZvNYIsTVPPwv7dfZdAfo/6H/2ZlZACK66NAOQtIApxkB/6pf/c+s+ACW8vemGJzUCyVTssrzNUK6yQ=="], + + "@react-native-harness/jest/@jest/test-result": ["@jest/test-result@30.3.0", "", { "dependencies": { "@jest/console": "30.3.0", "@jest/types": "30.3.0", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" } }, "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ=="], + + "@react-native-harness/jest/jest-message-util": ["jest-message-util@30.3.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.3.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "picomatch": "^4.0.3", "pretty-format": "30.3.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw=="], + + "@react-native-harness/jest/jest-util": ["jest-util@30.3.0", "", { "dependencies": { "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.3" } }, "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg=="], + + "@react-native-harness/tools/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + "@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@react-native/community-cli-plugin/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -2804,6 +2920,8 @@ "@react-native/eslint-config/eslint-config-prettier": ["eslint-config-prettier@8.10.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A=="], + "@react-native/metro-config/@react-native/js-polyfills": ["@react-native/js-polyfills@0.80.1", "", {}, "sha512-cWd5Cd2kBMRM37dor8N9Ck4X0NzjYM3m8K6HtjodcOdOvzpXfrfhhM56jdseTl5Z4iB+pohzPJpSmFJctmuIpA=="], + "@release-it-plugins/workspaces/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@release-it/conventional-changelog/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -2820,6 +2938,8 @@ "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + "abort-controller/event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -2968,6 +3088,8 @@ "jest-changed-files/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + "jest-changed-files/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "jest-circus/dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="], "jest-circus/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], @@ -2976,6 +3098,8 @@ "jest-circus/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + "jest-circus/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "jest-circus/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "jest-cli/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], @@ -3026,6 +3150,8 @@ "jest-runner/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + "jest-runner/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "jest-runtime/@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], "jest-runtime/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -3094,6 +3220,8 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + "normalize-package-data/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], @@ -3104,6 +3232,8 @@ "ora/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "package-json/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "pino/process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], @@ -3122,8 +3252,14 @@ "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "react-native/@react-native/assets-registry": ["@react-native/assets-registry@0.80.1", "", {}, "sha512-T3C8OthBHfpFIjaGFa0q6rc58T2AsJ+jKAa+qPquMKBtYGJMc75WgNbk/ZbPBxeity6FxZsmg3bzoUaWQo4Mow=="], + + "react-native/@react-native/js-polyfills": ["@react-native/js-polyfills@0.80.1", "", {}, "sha512-cWd5Cd2kBMRM37dor8N9Ck4X0NzjYM3m8K6HtjodcOdOvzpXfrfhhM56jdseTl5Z4iB+pohzPJpSmFJctmuIpA=="], + "react-native/@react-native/normalize-colors": ["@react-native/normalize-colors@0.80.1", "", {}, "sha512-YP12bjz0bzo2lFxZDOPkRJSOkcqAzXCQQIV1wd7lzCTXE0NJNwoaeNBobJvcPhiODEWUYCXPANrZveFhtFu5vw=="], + "react-native/@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.80.1", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.0.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-nqQAeHheSNZBV+syhLVMgKBZv+FhCANfxAWVvfEXZa4rm5jGHsj3yA9vqrh2lcJL3pjd7PW5nMX7TcuJThEAgQ=="], + "react-native/babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], @@ -3136,6 +3272,8 @@ "react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="], + "read-pkg/parse-json": ["parse-json@7.1.1", "", { "dependencies": { "@babel/code-frame": "^7.21.4", "error-ex": "^1.3.2", "json-parse-even-better-errors": "^3.0.0", "lines-and-columns": "^2.0.3", "type-fest": "^3.8.0" } }, "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw=="], "read-pkg/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -3210,6 +3348,50 @@ "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@apps/fs-experiment/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.82.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.82.1" } }, "sha512-wzmEz/RlR4SekqmaqeQjdMVh4LsnL9e62mrOikOOkHDQ3QN0nrKLuUDzXyYptVbxQ0IRua4pTm3efJLymDBoEg=="], + + "@apps/fs-experiment/@react-native/babel-preset/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="], + + "@apps/fs-experiment/@react-native/eslint-config/@react-native/eslint-plugin": ["@react-native/eslint-plugin@0.82.1", "", {}, "sha512-PU0ho8pNp24pdegIpYRAwppfO8z7werpoTts2CJ/wXYQ+ryZKa2M31DHW+kl+K3wwwqVqFKAzLh4t3sP/mOqMQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.38.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/type-utils": "8.38.0", "@typescript-eslint/utils": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser": ["@typescript-eslint/parser@8.38.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-config-prettier": ["eslint-config-prettier@8.10.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest": ["eslint-plugin-jest@29.15.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "jest": "*", "typescript": ">=4.8.4 <7.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin", "jest", "typescript"] }, "sha512-6BjyErCQauz3zfJvzLw/kAez2lf4LEpbHLvWBfEcG4EI0ZiRSwjoH2uZulMouU8kRkBH+S0rhqn11IhTvxKgKw=="], + + "@apps/fs-experiment/@react-native/metro-config/@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.82.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.82.1", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-kVQyYxYe1Da7cr7uGK9c44O6vTzM8YY3KW9CSLhhV1CGw7jmohU1HfLaUxDEmYfFZMc4Kj3JsIEbdUlaHMtprQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config": ["metro-config@0.83.5", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.5", "metro-cache": "0.83.5", "metro-core": "0.83.5", "metro-runtime": "0.83.5", "yaml": "^2.6.1" } }, "sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-runtime": ["metro-runtime@0.83.5", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA=="], + + "@apps/fs-experiment/react-native/@react-native/codegen": ["@react-native/codegen@0.82.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-ezXTN70ygVm9l2m0i+pAlct0RntoV4afftWMGUIeAWLgaca9qItQ54uOt32I/9dBJvzBibT33luIR/pBG0dQvg=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.82.1", "", { "dependencies": { "@react-native/dev-middleware": "0.82.1", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.1", "metro-config": "^0.83.1", "metro-core": "^0.83.1", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-H/eMdtOy9nEeX7YVeEG1N2vyCoifw3dr9OV8++xfUElNYV7LtSmJ6AqxZUUfxGJRDFPQvaU/8enmJlM/l11VxQ=="], + + "@apps/fs-experiment/react-native/@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.82.1", "", {}, "sha512-KkF/2T1NSn6EJ5ALNT/gx0MHlrntFHv8YdooH9OOGl9HQn5NM0ZmQSr86o5utJsGc7ME3R6p3SaQuzlsFDrn8Q=="], + + "@apps/fs-experiment/react-native/babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], + + "@apps/fs-experiment/react-native/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="], + + "@apps/fs-experiment/react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "@apps/fs-experiment/react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@apps/fs-experiment/react-native/memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], + + "@apps/fs-experiment/react-native/metro-runtime": ["metro-runtime@0.83.5", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA=="], + + "@apps/fs-experiment/react-native/metro-source-map": ["metro-source-map@0.83.5", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.5", "nullthrows": "^1.1.1", "ob1": "0.83.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ=="], + + "@apps/fs-experiment/react-native/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "@apps/fs-experiment/react-native/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@callstack/repack-dev-server/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.78.3", "", {}, "sha512-ImYGtEI9zsF/pietY45M8vd3OVWEkECbOngOhul0GVHECBsSHuOaQ/8PoxWl9Rps+8p1048aIMsPT9QzEtGwtQ=="], "@callstack/repack-dev-server/@react-native/dev-middleware/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -3476,6 +3658,16 @@ "@react-native-community/cli/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "@react-native-harness/jest/@jest/test-result/@jest/console": ["@jest/console@30.3.0", "", { "dependencies": { "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "jest-message-util": "30.3.0", "jest-util": "30.3.0", "slash": "^3.0.0" } }, "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww=="], + + "@react-native-harness/jest/@jest/test-result/@jest/types": ["@jest/types@30.3.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw=="], + + "@react-native-harness/jest/jest-message-util/@jest/types": ["@jest/types@30.3.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw=="], + + "@react-native-harness/jest/jest-message-util/pretty-format": ["pretty-format@30.3.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ=="], + + "@react-native-harness/jest/jest-util/@jest/types": ["@jest/types@30.3.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw=="], + "@react-native/dev-middleware/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], @@ -3618,12 +3810,16 @@ "jest-changed-files/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "jest-changed-files/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "jest-circus/jest-matcher-utils/jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], "jest-circus/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "jest-circus/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "jest-circus/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "jest-circus/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], "jest-circus/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -3682,6 +3878,8 @@ "jest-runner/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "jest-runner/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "jest-runtime/@jest/transform/babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], "jest-runtime/@jest/transform/write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], @@ -3744,6 +3942,8 @@ "ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "react-native/babel-jest/@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], @@ -3796,6 +3996,70 @@ "windows-release/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + "@apps/fs-experiment/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.82.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-ezXTN70ygVm9l2m0i+pAlct0RntoV4afftWMGUIeAWLgaca9qItQ54uOt32I/9dBJvzBibT33luIR/pBG0dQvg=="], + + "@apps/fs-experiment/@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="], + + "@apps/fs-experiment/@react-native/metro-config/@react-native/metro-babel-transformer/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro": ["metro@0.83.5", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.33.3", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.5", "metro-cache": "0.83.5", "metro-cache-key": "0.83.5", "metro-config": "0.83.5", "metro-core": "0.83.5", "metro-file-map": "0.83.5", "metro-resolver": "0.83.5", "metro-runtime": "0.83.5", "metro-source-map": "0.83.5", "metro-symbolicate": "0.83.5", "metro-transform-plugins": "0.83.5", "metro-transform-worker": "0.83.5", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro-cache": ["metro-cache@0.83.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.5" } }, "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro-core": ["metro-core@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.5" } }, "sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ=="], + + "@apps/fs-experiment/react-native/@react-native/codegen/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.82.1", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.82.1", "@react-native/debugger-shell": "0.82.1", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^6.2.3" } }, "sha512-wuOIzms/Qg5raBV6Ctf2LmgzEOCqdP3p1AYN4zdhMT110c39TVMbunpBaJxm0Kbt2HQ762MQViF9naxk7SBo4w=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro": ["metro@0.83.5", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.33.3", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.5", "metro-cache": "0.83.5", "metro-cache-key": "0.83.5", "metro-config": "0.83.5", "metro-core": "0.83.5", "metro-file-map": "0.83.5", "metro-resolver": "0.83.5", "metro-runtime": "0.83.5", "metro-source-map": "0.83.5", "metro-symbolicate": "0.83.5", "metro-transform-plugins": "0.83.5", "metro-transform-worker": "0.83.5", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro-config": ["metro-config@0.83.5", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.5", "metro-cache": "0.83.5", "metro-core": "0.83.5", "metro-runtime": "0.83.5", "yaml": "^2.6.1" } }, "sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro-core": ["metro-core@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.5" } }, "sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ=="], + + "@apps/fs-experiment/react-native/babel-jest/@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], + + "@apps/fs-experiment/react-native/babel-jest/babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], + + "@apps/fs-experiment/react-native/babel-jest/babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], + + "@apps/fs-experiment/react-native/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + + "@apps/fs-experiment/react-native/metro-source-map/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@apps/fs-experiment/react-native/metro-source-map/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@apps/fs-experiment/react-native/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA=="], + + "@apps/fs-experiment/react-native/metro-source-map/ob1": ["ob1@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg=="], + + "@apps/fs-experiment/react-native/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@apps/fs-experiment/react-native/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "@callstack/repack-dev-server/@react-native/dev-middleware/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "@callstack/repack-dev-server/@react-native/dev-middleware/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], @@ -3898,6 +4162,8 @@ "@react-native-community/cli/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "@react-native-harness/jest/jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "boxen/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -4022,6 +4288,152 @@ "windows-release/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "@apps/fs-experiment/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@apps/fs-experiment/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + + "@apps/fs-experiment/@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], + + "@apps/fs-experiment/@react-native/metro-config/@react-native/metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/hermes-parser": ["hermes-parser@0.33.3", "", { "dependencies": { "hermes-estree": "0.33.3" } }, "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-babel-transformer": ["metro-babel-transformer@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.33.3", "nullthrows": "^1.1.1" } }, "sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-cache-key": ["metro-cache-key@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-file-map": ["metro-file-map@0.83.5", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-resolver": ["metro-resolver@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-source-map": ["metro-source-map@0.83.5", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.5", "nullthrows": "^1.1.1", "ob1": "0.83.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-symbolicate": ["metro-symbolicate@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-transform-plugins": ["metro-transform-plugins@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-transform-worker": ["metro-transform-worker@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.5", "metro-babel-transformer": "0.83.5", "metro-cache": "0.83.5", "metro-cache-key": "0.83.5", "metro-minify-terser": "0.83.5", "metro-source-map": "0.83.5", "metro-transform-plugins": "0.83.5", "nullthrows": "^1.1.1" } }, "sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro-core/metro-resolver": ["metro-resolver@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A=="], + + "@apps/fs-experiment/react-native/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.82.1", "", {}, "sha512-a2O6M7/OZ2V9rdavOHyCQ+10z54JX8+B+apYKCQ6a9zoEChGTxUMG2YzzJ8zZJVvYf1ByWSNxv9Se0dca1hO9A=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/hermes-parser": ["hermes-parser@0.33.3", "", { "dependencies": { "hermes-estree": "0.33.3" } }, "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-babel-transformer": ["metro-babel-transformer@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.33.3", "nullthrows": "^1.1.1" } }, "sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-cache": ["metro-cache@0.83.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.5" } }, "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-cache-key": ["metro-cache-key@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-file-map": ["metro-file-map@0.83.5", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-resolver": ["metro-resolver@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-symbolicate": ["metro-symbolicate@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-transform-plugins": ["metro-transform-plugins@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-transform-worker": ["metro-transform-worker@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.5", "metro-babel-transformer": "0.83.5", "metro-cache": "0.83.5", "metro-cache-key": "0.83.5", "metro-minify-terser": "0.83.5", "metro-source-map": "0.83.5", "metro-transform-plugins": "0.83.5", "nullthrows": "^1.1.1" } }, "sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro-config/metro-cache": ["metro-cache@0.83.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.5" } }, "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro-core/metro-resolver": ["metro-resolver@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A=="], + + "@apps/fs-experiment/react-native/babel-jest/@jest/transform/jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], + + "@apps/fs-experiment/react-native/babel-jest/@jest/transform/jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], + + "@apps/fs-experiment/react-native/babel-jest/@jest/transform/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + + "@apps/fs-experiment/react-native/babel-jest/@jest/transform/write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + + "@apps/fs-experiment/react-native/babel-jest/babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + + "@apps/fs-experiment/react-native/babel-jest/babel-preset-jest/babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], + + "@apps/fs-experiment/react-native/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + + "@apps/fs-experiment/react-native/metro-source-map/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@apps/fs-experiment/react-native/metro-source-map/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@apps/fs-experiment/react-native/metro-source-map/@babel/traverse/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@apps/fs-experiment/react-native/metro-source-map/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@apps/fs-experiment/react-native/metro-source-map/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@apps/fs-experiment/react-native/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "@commitlint/parse/conventional-commits-parser/meow/normalize-package-data/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "@commitlint/parse/conventional-commits-parser/meow/normalize-package-data/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -4110,6 +4522,68 @@ "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + "@apps/fs-experiment/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/hermes-parser/hermes-estree": ["hermes-estree@0.33.3", "", {}, "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-source-map/ob1": ["ob1@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q=="], + + "@apps/fs-experiment/@react-native/metro-config/metro-config/metro/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.33.3", "", {}, "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q=="], + + "@apps/fs-experiment/react-native/@react-native/community-cli-plugin/metro/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "@apps/fs-experiment/react-native/babel-jest/@jest/transform/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "@apps/fs-experiment/react-native/babel-jest/@jest/transform/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "@apps/fs-experiment/react-native/babel-jest/@jest/transform/write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@apps/fs-experiment/react-native/babel-jest/babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@apps/fs-experiment/react-native/metro-source-map/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@commitlint/parse/conventional-commits-parser/meow/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "@commitlint/parse/conventional-commits-parser/meow/read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -4144,7 +4618,11 @@ "logkitty/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "read-pkg-up/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@apps/fs-experiment/@react-native/eslint-config/eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@commitlint/parse/conventional-commits-parser/meow/normalize-package-data/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -4166,8 +4644,6 @@ "@commitlint/read/git-raw-commits/meow/read-pkg-up/read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], - "@commitlint/parse/conventional-commits-parser/meow/read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "@commitlint/read/git-raw-commits/meow/read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/packages/react-native-sandbox/README.md b/packages/react-native-sandbox/README.md index 62d0e79..6098bbb 100644 --- a/packages/react-native-sandbox/README.md +++ b/packages/react-native-sandbox/README.md @@ -56,6 +56,7 @@ import SandboxReactNativeView from '@callstack/react-native-sandbox'; | `initialProperties` | `object` | :white_large_square: | `{}` | Initial props for the sandboxed app | | `launchOptions` | `object` | :white_large_square: | `{}` | Launch configuration options | | `allowedTurboModules` | `string[]` | :white_large_square: | [check here](https://github.com/callstackincubator/react-native-sandbox/blob/main/packages/react-native-sandbox/src/index.tsx#L18) | Additional TurboModules to allow | +| `turboModuleSubstitutions` | `Record` | :white_large_square: | `undefined` | Map of module name substitutions (requested β†’ resolved). Substituted modules are implicitly allowed. | | `onMessage` | `function` | :white_large_square: | `undefined` | Callback for messages from sandbox | | `onError` | `function` | :white_large_square: | `undefined` | Callback for sandbox errors | | `style` | `ViewStyle` | :white_large_square: | `undefined` | Container styling | @@ -102,6 +103,27 @@ Use `allowedTurboModules` to control which native modules the sandbox can access > Note: This filtering works with both legacy native modules and new TurboModules, ensuring compatibility across React Native versions. +#### TurboModule Substitutions + +Use `turboModuleSubstitutions` to transparently replace a module with a sandbox-aware implementation. When sandbox JS requests a module by name, the substitution map redirects it to a different native module: + +```tsx + +``` + +Substituted modules are **implicitly allowed** and don't need to be listed in `allowedTurboModules`. If the resolved module conforms to `RCTSandboxAwareModule` (ObjC) or `ISandboxAwareModule` (C++), it receives sandbox context (origin, requested name, resolved name) after instantiation β€” enabling per-origin data scoping. + +Changing `turboModuleSubstitutions` at runtime triggers a full re-instantiation of the sandbox's React Native runtime, ensuring TurboModules are re-resolved with the new configuration. + +See the [`apps/fs-experiment`](https://github.com/callstackincubator/react-native-sandbox/tree/main/apps/fs-experiment) example for a working demonstration. + #### Message Origin Control Use `allowedOrigins` to specify which sandbox origins are allowed to send messages to this sandbox: diff --git a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxAwareModule.kt b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxAwareModule.kt new file mode 100644 index 0000000..534e7b9 --- /dev/null +++ b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxAwareModule.kt @@ -0,0 +1,28 @@ +package io.callstack.rnsandbox + +/** + * Interface for native modules that need sandbox-specific configuration. + * + * When a native module is provided as a substitution in the sandbox, + * the sandbox delegate checks if it implements this interface and calls + * [configureSandbox] with context about the sandbox instance. + * + * This enables modules to scope their behavior per sandbox origin, + * e.g. sandboxing file system access to a per-origin directory or + * isolating AsyncStorage keys by origin. + */ +interface SandboxAwareModule { + /** + * Called by the sandbox delegate after module instantiation to provide + * sandbox-specific context. + * + * @param origin The sandbox origin identifier + * @param requestedName The module name sandbox JS code requested + * @param resolvedName The actual module name that was resolved + */ + fun configureSandbox( + origin: String, + requestedName: String, + resolvedName: String, + ) +} diff --git a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeDelegate.kt b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeDelegate.kt index 66648af..c008e5d 100644 --- a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeDelegate.kt +++ b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeDelegate.kt @@ -35,6 +35,33 @@ class SandboxReactNativeDelegate( private const val TAG = "SandboxRNDelegate" private val sharedHosts = mutableMapOf() + private val registeredSubstitutionPackages = mutableListOf() + private val registeredHostPackages = mutableListOf() + + /** + * Register ReactPackage instances that provide substitution modules. + * Call this from your Application.onCreate() before any sandbox views load. + */ + @JvmStatic + fun registerSubstitutionPackages(vararg packages: ReactPackage) { + registeredSubstitutionPackages.addAll(packages) + } + + /** + * Register the host app's autolinked ReactPackage instances so that + * allowed (non-substituted) third-party modules can be resolved inside + * the sandbox. Without this, only modules from MainReactPackage (RN + * built-ins) are available. + * + * Typically called from Application.onCreate(): + * ``` + * SandboxReactNativeDelegate.registerHostPackages(PackageList(this).packages) + * ``` + */ + @JvmStatic + fun registerHostPackages(packages: List) { + registeredHostPackages.addAll(packages) + } private data class SharedReactHost( val reactHost: ReactHostImpl, @@ -47,6 +74,7 @@ class SandboxReactNativeDelegate( var jsBundleSource: String = "" var allowedTurboModules: Set = emptySet() + var turboModuleSubstitutions: Map = emptyMap() var allowedOrigins: Set = emptySet() @JvmField var hasOnMessageHandler: Boolean = false @@ -89,9 +117,21 @@ class SandboxReactNativeDelegate( } else { sandboxContext = SandboxContextWrapper(context, origin) + val capturedSubstitutions = turboModuleSubstitutions.toMap() + val capturedSubstitutionPackages = registeredSubstitutionPackages.toList() + val capturedHostPackages = registeredHostPackages.toList() + val capturedOrigin = origin + val packages: List = listOf( - FilteredReactPackage(MainReactPackage(), capturedAllowedModules), + FilteredReactPackage( + MainReactPackage(), + capturedHostPackages, + capturedAllowedModules, + capturedSubstitutions, + capturedSubstitutionPackages, + capturedOrigin, + ), ) val bundleLoader = createBundleLoader(capturedBundleSource) ?: return null @@ -197,7 +237,7 @@ class SandboxReactNativeDelegate( Log.d(TAG, "Reloaded sandbox '$origin' with new bundle source via reflection") return true } catch (e: Exception) { - Log.w(TAG, "Reflection-based bundle reload failed, falling back to full rebuild: ${e.message}") + Log.d(TAG, "Reflection-based bundle reload failed, falling back to full rebuild: ${e.message}") return false } } @@ -346,23 +386,90 @@ class SandboxReactNativeDelegate( private class FilteredReactPackage( private val delegate: MainReactPackage, + private val hostPackages: List, private val allowedModules: Set, + private val substitutions: Map, + private val substitutionPackages: List, + private val origin: String, ) : BaseReactPackage() { + private val substitutedInstances = java.util.concurrent.ConcurrentHashMap() + + private val effectiveAllowed: Set by lazy { + allowedModules + substitutions.keys + } + override fun getModule( name: String, reactContext: ReactApplicationContext, ): NativeModule? { - if (!allowedModules.contains(name)) { - Log.w(TAG, "Blocked module '$name' β€” not in allowedTurboModules") + val resolvedName = substitutions[name] + if (resolvedName != null) { + substitutedInstances[name]?.let { return it } + + for (pkg in substitutionPackages) { + val module = + if (pkg is BaseReactPackage) { + pkg.getModule(resolvedName, reactContext) + } else { + pkg.createNativeModules(reactContext).firstOrNull { it.name == resolvedName } + } + if (module != null) { + if (module is SandboxAwareModule) { + module.configureSandbox(origin, name, resolvedName) + } + substitutedInstances[name] = module + Log.d(TAG, "Substituted '$name' -> '$resolvedName' (${module.javaClass.simpleName})") + return module + } + } + Log.w(TAG, "Substitution target '$resolvedName' not found in any package for '$name'") + return null + } + + if (!effectiveAllowed.contains(name)) { return null } - return delegate.getModule(name, reactContext) + + delegate.getModule(name, reactContext)?.let { return it } + + for (pkg in hostPackages) { + val module = + if (pkg is BaseReactPackage) { + pkg.getModule(name, reactContext) + } else { + pkg.createNativeModules(reactContext).firstOrNull { it.name == name } + } + if (module != null) return module + } + return null } override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { val delegateProvider = delegate.getReactModuleInfoProvider() + val hostProviders = + hostPackages.mapNotNull { + (it as? BaseReactPackage)?.getReactModuleInfoProvider() + } + val substitutionProviders = + substitutionPackages.mapNotNull { + (it as? BaseReactPackage)?.getReactModuleInfoProvider() + } return ReactModuleInfoProvider { - delegateProvider.getReactModuleInfos().filterKeys { allowedModules.contains(it) } + val infos = + delegateProvider + .getReactModuleInfos() + .filterKeys { effectiveAllowed.contains(it) } + .toMutableMap() + for (provider in hostProviders) { + infos.putAll(provider.getReactModuleInfos().filterKeys { effectiveAllowed.contains(it) }) + } + for ((requestedName, resolvedName) in substitutions) { + for (provider in substitutionProviders) { + val subInfos = provider.getReactModuleInfos() + subInfos[resolvedName]?.let { infos[requestedName] = it } + } + } + infos } } diff --git a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeView.kt b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeView.kt index 8619e91..0c1e938 100644 --- a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeView.kt +++ b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeView.kt @@ -27,6 +27,36 @@ class SandboxReactNativeView( } } + /** + * Fabric manages our dimensions but not our children's (they come from a + * separate ReactHost). Force children to fill the space Fabric gave us. + */ + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int, + ) { + super.onLayout(changed, left, top, right, bottom) + val w = right - left + val h = bottom - top + for (i in 0 until childCount) { + getChildAt(i).layout(0, 0, w, h) + } + } + + override fun requestLayout() { + super.requestLayout() + post { + measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), + ) + layout(left, top, right, bottom) + } + } + fun emitOnMessage(data: WritableMap) { val reactContext = context as? ReactContext ?: return val surfaceId = UIManagerHelper.getSurfaceId(reactContext) diff --git a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeViewManager.kt b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeViewManager.kt index 1922c00..f6b0b42 100644 --- a/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeViewManager.kt +++ b/packages/react-native-sandbox/android/src/main/java/io/callstack/rnsandbox/SandboxReactNativeViewManager.kt @@ -112,7 +112,35 @@ class SandboxReactNativeViewManager : it.getString(i)?.let { name -> modules.add(name) } } } - view.delegate?.allowedTurboModules = modules + val delegate = view.delegate ?: return + if (delegate.allowedTurboModules == modules) return + delegate.allowedTurboModules = modules + if (view.childCount > 0) { + scheduleLoad(view) + } + } + + @ReactProp(name = "turboModuleSubstitutions") + override fun setTurboModuleSubstitutions( + view: SandboxReactNativeView, + value: Dynamic, + ) { + val subs = mutableMapOf() + if (!value.isNull && value.type == ReadableType.Map) { + val map = value.asMap() ?: return + val it = map.keySetIterator() + while (it.hasNextKey()) { + val key = it.nextKey() + val v = map.getString(key) + if (v != null) subs[key] = v + } + } + val delegate = view.delegate ?: return + if (delegate.turboModuleSubstitutions == subs) return + delegate.turboModuleSubstitutions = subs + if (view.childCount > 0) { + scheduleLoad(view) + } } @ReactProp(name = "allowedOrigins") @@ -202,6 +230,7 @@ class SandboxReactNativeViewManager : FrameLayout.LayoutParams.MATCH_PARENT, ), ) + view.requestLayout() } private fun dynamicToBundle(dynamic: Dynamic): Bundle? { diff --git a/packages/react-native-sandbox/cxx/ISandboxAwareModule.h b/packages/react-native-sandbox/cxx/ISandboxAwareModule.h new file mode 100644 index 0000000..6a4b422 --- /dev/null +++ b/packages/react-native-sandbox/cxx/ISandboxAwareModule.h @@ -0,0 +1,64 @@ +#pragma once + +#ifdef __cplusplus + +#include + +namespace rnsandbox { + +/** + * Context information provided to sandbox-aware TurboModules. + * Contains the sandbox identity and module mapping details needed + * for scoping module behavior per sandbox instance. + */ +struct SandboxContext { + /** The origin identifier of the sandbox instance */ + std::string origin; + + /** The module name that sandbox JS code requested (e.g. "RNCAsyncStorage") */ + std::string requestedModuleName; + + /** The actual module name that was resolved via substitution (e.g. + * "SandboxedAsyncStorage") */ + std::string resolvedModuleName; +}; + +/** + * Interface for TurboModules that need sandbox-specific configuration. + * + * When a TurboModule is provided as a substitution in the sandbox, + * the sandbox delegate will check if the module implements this interface + * and call configureSandbox() with the relevant context. + * + * This enables modules to scope their behavior per sandbox origin, + * e.g. sandboxing file system access to a per-origin directory or + * isolating AsyncStorage keys by origin. + * + * Usage: + * @code + * class SandboxedAsyncStorage : public TurboModule, public ISandboxAwareModule + * { public: void configureSandbox(const SandboxContext& context) override { + * // Scope storage to this sandbox's origin + * storagePrefix_ = context.origin; + * } + * }; + * @endcode + */ +class ISandboxAwareModule { + public: + virtual ~ISandboxAwareModule() = default; + + /** + * Called by the sandbox delegate after module instantiation to provide + * sandbox-specific context. Implementations should use this to scope + * their behavior (storage, file paths, etc.) to the given sandbox. + * + * @param context The sandbox context containing origin and module mapping + * info + */ + virtual void configureSandbox(const SandboxContext& context) = 0; +}; + +} // namespace rnsandbox + +#endif // __cplusplus diff --git a/packages/react-native-sandbox/cxx/StubTurboModuleCxx.cpp b/packages/react-native-sandbox/cxx/StubTurboModuleCxx.cpp index 3b67042..e3bdc85 100644 --- a/packages/react-native-sandbox/cxx/StubTurboModuleCxx.cpp +++ b/packages/react-native-sandbox/cxx/StubTurboModuleCxx.cpp @@ -8,14 +8,18 @@ StubTurboModuleCxx::StubTurboModuleCxx( std::shared_ptr jsInvoker) : facebook::react::TurboModule("StubTurboModuleCxx", jsInvoker), moduleName_(moduleName) { +#if DEBUG logBlockedAccess("constructor"); +#endif } facebook::jsi::Value StubTurboModuleCxx::get( facebook::jsi::Runtime& runtime, const facebook::jsi::PropNameID& propName) { std::string methodName = propName.utf8(runtime); +#if DEBUG logBlockedAccess(methodName); +#endif return createStubFunction(runtime, methodName); } @@ -35,15 +39,28 @@ facebook::jsi::Function StubTurboModuleCxx::createStubFunction( facebook::jsi::PropNameID::forAscii(runtime, methodName.c_str()), 0, [moduleName = moduleName_, methodName]( - facebook::jsi::Runtime&, + facebook::jsi::Runtime& rt, const facebook::jsi::Value&, const facebook::jsi::Value*, size_t) -> facebook::jsi::Value { +#if DEBUG SANDBOX_LOG_WARN( "[StubTurboModuleCxx] Method call '%s' blocked on module '%s'.", methodName.c_str(), moduleName.c_str()); + + auto errorMsg = std::string("Module '") + moduleName + + "' is blocked. Method '" + methodName + + "' is not available in this sandbox."; + auto Promise = rt.global().getPropertyAsFunction(rt, "Promise"); + auto reject = Promise.getPropertyAsFunction(rt, "reject"); + auto Error = rt.global().getPropertyAsFunction(rt, "Error"); + auto error = Error.callAsConstructor( + rt, facebook::jsi::String::createFromUtf8(rt, errorMsg)); + return reject.callWithThis(rt, Promise, error); +#else return facebook::jsi::Value::undefined(); +#endif }); } diff --git a/packages/react-native-sandbox/ios/RCTSandboxAwareModule.h b/packages/react-native-sandbox/ios/RCTSandboxAwareModule.h new file mode 100644 index 0000000..411fc73 --- /dev/null +++ b/packages/react-native-sandbox/ios/RCTSandboxAwareModule.h @@ -0,0 +1,41 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * ObjC protocol equivalent of ISandboxAwareModule for ObjC TurboModules. + * + * When a TurboModule substitution resolves an ObjC module, the sandbox delegate + * checks if the module conforms to this protocol and calls configureSandbox: + * with context about the sandbox instance. + * + * @code + * @interface SandboxedAsyncStorage : NSObject + * @end + * + * @implementation SandboxedAsyncStorage + * - (void)configureSandboxWithOrigin:(NSString *)origin + * requestedName:(NSString *)requestedName + * resolvedName:(NSString *)resolvedName { + * self.storageDirectory = [basePath stringByAppendingPathComponent:origin]; + * } + * @end + * @endcode + */ +@protocol RCTSandboxAwareModule + +/** + * Called by the sandbox delegate after module instantiation to provide + * sandbox-specific context. + * + * @param origin The sandbox origin identifier + * @param requestedName The module name sandbox JS code requested + * @param resolvedName The actual module name that was resolved + */ +- (void)configureSandboxWithOrigin:(NSString *)origin + requestedName:(NSString *)requestedName + resolvedName:(NSString *)resolvedName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-sandbox/ios/SandboxReactNativeDelegate.h b/packages/react-native-sandbox/ios/SandboxReactNativeDelegate.h index 1ec162d..d23f780 100644 --- a/packages/react-native-sandbox/ios/SandboxReactNativeDelegate.h +++ b/packages/react-native-sandbox/ios/SandboxReactNativeDelegate.h @@ -11,6 +11,7 @@ #import #import +#include #include #include @@ -44,6 +45,18 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readwrite) std::set allowedOrigins; +/** + * Sets the TurboModule substitution map for this sandbox instance. + * Keys are module names that sandbox JS code requests, values are the actual + * native module names to resolve instead. Substituted modules are implicitly allowed. + * + * Example: {"RNCAsyncStorage": "SandboxedAsyncStorage"} means when sandbox JS + * requests RNCAsyncStorage, the delegate resolves SandboxedAsyncStorage instead + * and configures it with the sandbox context (origin, etc.) if it implements + * ISandboxAwareModule. + */ +@property (nonatomic, readwrite) std::map turboModuleSubstitutions; + /** * Initializes the delegate. * @return Initialized delegate instance with filtered module access diff --git a/packages/react-native-sandbox/ios/SandboxReactNativeDelegate.mm b/packages/react-native-sandbox/ios/SandboxReactNativeDelegate.mm index d58980b..da75952 100644 --- a/packages/react-native-sandbox/ios/SandboxReactNativeDelegate.mm +++ b/packages/react-native-sandbox/ios/SandboxReactNativeDelegate.mm @@ -14,15 +14,19 @@ #include #include +#import #import #import #import #import +#import #import #import #include +#include "ISandboxAwareModule.h" +#import "RCTSandboxAwareModule.h" #include "SandboxDelegateWrapper.h" #include "SandboxRegistry.h" #import "StubTurboModuleCxx.h" @@ -31,6 +35,30 @@ namespace TurboModuleConvertUtils = facebook::react::TurboModuleConvertUtils; using namespace facebook::react; +class SandboxNativeMethodCallInvoker : public NativeMethodCallInvoker { + dispatch_queue_t methodQueue_; + + public: + explicit SandboxNativeMethodCallInvoker(dispatch_queue_t methodQueue) : methodQueue_(methodQueue) {} + + void invokeAsync(const std::string &, std::function &&work) noexcept override + { + if (methodQueue_ == RCTJSThread) { + work(); + return; + } + __block auto retainedWork = std::move(work); + dispatch_async(methodQueue_, ^{ + retainedWork(); + }); + } + + void invokeSync(const std::string &, std::function &&work) override + { + work(); + } +}; + static void stubJsiFunction(jsi::Runtime &runtime, jsi::Object &object, const char *name) { object.setProperty( @@ -57,8 +85,10 @@ @interface SandboxReactNativeDelegate () { std::shared_ptr _delegateWrapper; std::set _allowedTurboModules; std::set _allowedOrigins; + std::map _turboModuleSubstitutions; std::string _origin; std::string _jsBundleSource; + NSMutableDictionary> *_substitutedModuleInstances; } - (void)cleanupResources; @@ -81,6 +111,7 @@ - (instancetype)init if (self = [super init]) { _hasOnMessageHandler = NO; _hasOnErrorHandler = NO; + _substitutedModuleInstances = [NSMutableDictionary new]; self.dependencyProvider = [[RCTAppDependencyProvider alloc] init]; } return self; @@ -92,6 +123,8 @@ - (void)cleanupResources _rctInstance = nil; _allowedTurboModules.clear(); _allowedOrigins.clear(); + _turboModuleSubstitutions.clear(); + [_substitutedModuleInstances removeAllObjects]; if (_delegateWrapper) { _delegateWrapper->invalidate(); _delegateWrapper.reset(); @@ -167,6 +200,16 @@ - (void)setAllowedTurboModules:(std::set)allowedTurboModules _allowedTurboModules = allowedTurboModules; } +- (std::map)turboModuleSubstitutions +{ + return _turboModuleSubstitutions; +} + +- (void)setTurboModuleSubstitutions:(std::map)turboModuleSubstitutions +{ + _turboModuleSubstitutions = turboModuleSubstitutions; +} + - (void)dealloc { if (_delegateWrapper) { @@ -299,22 +342,251 @@ - (void)hostDidStart:(RCTHost *)host }]; } -#pragma mark - RCTTurboModuleManagerDelegate +/** + * RCTTurboModuleManagerDelegate resolution order (called by RCTTurboModuleManager): + * + * PRIORITY 1 β€” getTurboModule:jsInvoker: + * Called first. Returns a fully constructed C++ TurboModule (shared_ptr). + * If non-null, resolution stops here β€” nothing else is called. + * This is the primary path for C++ TurboModules and our ObjC substitution fallback. + * + * PRIORITY 2 β€” getModuleClassFromName: + * Called if getTurboModule returned nullptr. Provides the ObjC class for a module name. + * The TurboModuleManager then calls getModuleInstanceFromClass: with this class. + * + * PRIORITY 3 β€” getModuleInstanceFromClass: + * Called with the class from step 2 (or the auto-registered class). + * Creates and returns an ObjC module instance. The TurboModuleManager then wraps it + * in an ObjCInteropTurboModule internally and sets up its methodQueue via KVC. + * NOTE: This path goes through RCTInstance as a weak delegate intermediary, which + * can become nil β€” causing a second unconfigured instance. That's why we prefer + * handling ObjC substitutions in getTurboModule:jsInvoker: (priority 1) instead. + * + * PRIORITY 4 β€” getModuleProvider: + * Legacy/alternative path. Called by some internal flows to get a module instance + * by name string. Similar role to getModuleInstanceFromClass but name-based. + */ -- (id)getModuleProvider:(const char *)name -{ - return _allowedTurboModules.contains(name) ? [super getModuleProvider:name] : nullptr; -} +#pragma mark - RCTTurboModuleManagerDelegate +// PRIORITY 1 - (std::shared_ptr)getTurboModule:(const std::string &)name jsInvoker:(std::shared_ptr)jsInvoker { + auto it = _turboModuleSubstitutions.find(name); + if (it != _turboModuleSubstitutions.end()) { + const std::string &resolvedName = it->second; + + // Try C++ TurboModule first (e.g. codegen-generated spec) + auto cxxModule = [super getTurboModule:resolvedName jsInvoker:jsInvoker]; + if (cxxModule) { + if (auto sandboxAware = std::dynamic_pointer_cast(cxxModule)) { + sandboxAware->configureSandbox({ + .origin = _origin, + .requestedModuleName = name, + .resolvedModuleName = resolvedName, + }); + } + return cxxModule; + } + + return [self _createObjCTurboModuleForSubstitution:name resolvedName:resolvedName jsInvoker:jsInvoker]; + } + if (_allowedTurboModules.contains(name)) { return [super getTurboModule:name jsInvoker:jsInvoker]; - } else { - // Return C++ stub instead of nullptr - return std::make_shared(name, jsInvoker); } + + return std::make_shared(name, jsInvoker); +} + +// PRIORITY 2 +- (Class)getModuleClassFromName:(const char *)name +{ + std::string nameStr(name); + + auto it = _turboModuleSubstitutions.find(nameStr); + if (it != _turboModuleSubstitutions.end()) { + NSString *resolvedName = [NSString stringWithUTF8String:it->second.c_str()]; + for (Class moduleClass in RCTGetModuleClasses()) { + if ([[moduleClass moduleName] isEqualToString:resolvedName]) { + return moduleClass; + } + } + } + + return nullptr; +} + +// PRIORITY 3 +- (id)getModuleInstanceFromClass:(Class)moduleClass +{ + NSString *moduleName = [moduleClass moduleName]; + if (!moduleName) { + return nullptr; + } + + id cached = _substitutedModuleInstances[moduleName]; + if (cached) { + return (id)cached; + } + + std::string moduleNameStr = [moduleName UTF8String]; + bool isSubstitutionTarget = false; + std::string requestedName; + + for (auto &pair : _turboModuleSubstitutions) { + if (pair.second == moduleNameStr) { + isSubstitutionTarget = true; + requestedName = pair.first; + break; + } + } + + if (!isSubstitutionTarget) { + return nullptr; + } + + id module = [moduleClass new]; + + if ([(id)module conformsToProtocol:@protocol(RCTSandboxAwareModule)]) { + NSString *originNS = [NSString stringWithUTF8String:_origin.c_str()]; + NSString *requestedNameNS = [NSString stringWithUTF8String:requestedName.c_str()]; + [(id)module configureSandboxWithOrigin:originNS + requestedName:requestedNameNS + resolvedName:moduleName]; + } + + _substitutedModuleInstances[moduleName] = module; + return (id)module; +} + +// PRIORITY 4 +- (id)getModuleProvider:(const char *)name +{ + std::string nameStr(name); + + auto it = _turboModuleSubstitutions.find(nameStr); + if (it != _turboModuleSubstitutions.end()) { + NSString *resolvedName = [NSString stringWithUTF8String:it->second.c_str()]; + + id cached = _substitutedModuleInstances[resolvedName]; + if (cached) { + return (id)cached; + } + + // Try the dependency provider first (for Codegen TurboModules) + id provider = [super getModuleProvider:it->second.c_str()]; + + if (!provider) { + for (Class moduleClass in RCTGetModuleClasses()) { + if ([[moduleClass moduleName] isEqualToString:resolvedName]) { + provider = [moduleClass new]; + break; + } + } + } + + if (!provider) { + return nullptr; + } + + if ([(id)provider conformsToProtocol:@protocol(RCTSandboxAwareModule)]) { + NSString *originNS = [NSString stringWithUTF8String:_origin.c_str()]; + NSString *requestedNameNS = [NSString stringWithUTF8String:nameStr.c_str()]; + [(id)provider configureSandboxWithOrigin:originNS + requestedName:requestedNameNS + resolvedName:resolvedName]; + } + + if ([(id)provider conformsToProtocol:@protocol(RCTBridgeModule)]) { + _substitutedModuleInstances[resolvedName] = (id)provider; + } + + return provider; + } + + return _allowedTurboModules.contains(nameStr) ? [super getModuleProvider:name] : nullptr; +} + +- (std::shared_ptr) + _createObjCTurboModuleForSubstitution:(const std::string &)requestedName + resolvedName:(const std::string &)resolvedName + jsInvoker:(std::shared_ptr)jsInvoker +{ + NSString *resolvedNameNS = [NSString stringWithUTF8String:resolvedName.c_str()]; + + id cached = _substitutedModuleInstances[resolvedNameNS]; + if (cached && [(id)cached conformsToProtocol:@protocol(RCTTurboModule)]) { + return [self _wrapObjCModule:cached moduleName:requestedName jsInvoker:jsInvoker]; + } + + Class moduleClass = nil; + for (Class cls in RCTGetModuleClasses()) { + if ([[cls moduleName] isEqualToString:resolvedNameNS]) { + moduleClass = cls; + break; + } + } + + if (!moduleClass) { + return nullptr; + } + + id instance = [moduleClass new]; + + if ([(id)instance conformsToProtocol:@protocol(RCTSandboxAwareModule)]) { + NSString *originNS = [NSString stringWithUTF8String:_origin.c_str()]; + NSString *requestedNameNS = [NSString stringWithUTF8String:requestedName.c_str()]; + [(id)instance configureSandboxWithOrigin:originNS + requestedName:requestedNameNS + resolvedName:resolvedNameNS]; + } + + _substitutedModuleInstances[resolvedNameNS] = instance; + + if (![(id)instance conformsToProtocol:@protocol(RCTTurboModule)]) { + return nullptr; + } + + return [self _wrapObjCModule:instance moduleName:requestedName jsInvoker:jsInvoker]; +} + +- (std::shared_ptr)_wrapObjCModule:(id)instance + moduleName:(const std::string &)moduleName + jsInvoker: + (std::shared_ptr)jsInvoker +{ + dispatch_queue_t methodQueue = nil; + BOOL hasMethodQueueGetter = [instance respondsToSelector:@selector(methodQueue)]; + if (hasMethodQueueGetter) { + methodQueue = [instance methodQueue]; + } + + if (!methodQueue) { + NSString *label = [NSString stringWithFormat:@"com.sandbox.%s", moduleName.c_str()]; + methodQueue = dispatch_queue_create(label.UTF8String, DISPATCH_QUEUE_SERIAL); + + if (hasMethodQueueGetter) { + @try { + [(id)instance setValue:methodQueue forKey:@"methodQueue"]; + } @catch (NSException *exception) { + RCTLogError(@"[Sandbox] Failed to set methodQueue on module '%s': %@", moduleName.c_str(), exception.reason); + } + } + } + + auto nativeInvoker = std::make_shared(methodQueue); + + facebook::react::ObjCTurboModule::InitParams params = { + .moduleName = moduleName, + .instance = instance, + .jsInvoker = jsInvoker, + .nativeMethodCallInvoker = nativeInvoker, + .isSyncModule = methodQueue == RCTJSThread, + .shouldVoidMethodsExecuteSync = false, + }; + return [(id)instance getTurboModule:params]; } - (jsi::Function)createPostMessageFunction:(jsi::Runtime &)runtime @@ -368,7 +640,6 @@ - (void)hostDidStart:(RCTHost *)host jsi::Function jsonStringify = jsonObject.getPropertyAsFunction(rt, "stringify"); jsi::Value jsonResult = jsonStringify.call(rt, messageArg); std::string messageJson = jsonResult.getString(rt).utf8(rt); - NSString *messageNS = [NSString stringWithUTF8String:messageJson.c_str()]; // Route message to specific sandbox BOOL success = [self routeMessage:messageJson toSandbox:targetOrigin]; diff --git a/packages/react-native-sandbox/ios/SandboxReactNativeViewComponentView.mm b/packages/react-native-sandbox/ios/SandboxReactNativeViewComponentView.mm index 9c22f93..cb69086 100644 --- a/packages/react-native-sandbox/ios/SandboxReactNativeViewComponentView.mm +++ b/packages/react-native-sandbox/ios/SandboxReactNativeViewComponentView.mm @@ -94,6 +94,18 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & [self.reactNativeDelegate setAllowedOrigins:allowedOrigins]; } + if (oldViewProps.turboModuleSubstitutions != newViewProps.turboModuleSubstitutions) { + std::map subs; + if (newViewProps.turboModuleSubstitutions.isObject()) { + for (const auto &pair : newViewProps.turboModuleSubstitutions.items()) { + if (pair.first.isString() && pair.second.isString()) { + subs[pair.first.getString()] = pair.second.getString(); + } + } + } + [self.reactNativeDelegate setTurboModuleSubstitutions:subs]; + } + self.reactNativeDelegate.hasOnMessageHandler = newViewProps.hasOnMessageHandler; self.reactNativeDelegate.hasOnErrorHandler = newViewProps.hasOnErrorHandler; @@ -101,7 +113,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & [self updateEventEmitterIfNeeded]; } - if (oldViewProps.componentName != newViewProps.componentName || + BOOL turboModuleConfigChanged = oldViewProps.allowedTurboModules != newViewProps.allowedTurboModules || + oldViewProps.turboModuleSubstitutions != newViewProps.turboModuleSubstitutions; + + if (turboModuleConfigChanged) { + self.reactNativeFactory = nil; + } + + if (turboModuleConfigChanged || oldViewProps.componentName != newViewProps.componentName || oldViewProps.initialProperties != newViewProps.initialProperties || oldViewProps.launchOptions != newViewProps.launchOptions) { [self scheduleReactViewLoad]; @@ -162,7 +181,6 @@ - (void)loadReactNativeView launchOptions = (NSDictionary *)convertFollyDynamicToId(props.launchOptions); } - // Use existing delegate (properties already updated in updateProps) if (!self.reactNativeFactory) { self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self.reactNativeDelegate]; } diff --git a/packages/react-native-sandbox/specs/NativeSandboxReactNativeView.ts b/packages/react-native-sandbox/specs/NativeSandboxReactNativeView.ts index ea6dac0..f334ee4 100644 --- a/packages/react-native-sandbox/specs/NativeSandboxReactNativeView.ts +++ b/packages/react-native-sandbox/specs/NativeSandboxReactNativeView.ts @@ -59,6 +59,13 @@ export interface NativeProps extends ViewProps { /** Array of TurboModule names allowed in the sandbox */ allowedTurboModules?: readonly string[] + /** + * Map of TurboModule substitutions for this sandbox. + * Keys are module names that sandbox JS requests, values are the actual + * native module names to resolve instead. Substituted modules are implicitly allowed. + */ + turboModuleSubstitutions?: CodegenTypes.UnsafeMixed + /** Array of sandbox origins that are allowed to send messages to this sandbox */ allowedOrigins?: readonly string[] diff --git a/packages/react-native-sandbox/src/index.tsx b/packages/react-native-sandbox/src/index.tsx index 5610381..0f77bb5 100644 --- a/packages/react-native-sandbox/src/index.tsx +++ b/packages/react-native-sandbox/src/index.tsx @@ -16,6 +16,7 @@ import NativeSandboxReactNativeView, { } from '../specs/NativeSandboxReactNativeView' const SANDBOX_TURBOMODULES_WHITELIST = [ + 'NativeDOMCxx', 'NativeMicrotasksCxx', 'NativePerformanceCxx', 'RedBox', @@ -102,6 +103,15 @@ export interface SandboxReactNativeViewProps extends ViewProps { */ allowedTurboModules?: string[] + /** + * Map of TurboModule substitutions for this sandbox instance. + * Keys are the module names that sandbox JS code requests, + * values are the actual native module names to resolve instead. + * Substituted modules are implicitly allowed and don't need to be + * listed in allowedTurboModules. + */ + turboModuleSubstitutions?: Record + /** * Array of sandbox origins that are allowed to send messages to this sandbox. * If not provided or empty, no other sandboxes will be allowed to send messages.