Note: if you're looking for the quickest way to get SQLite working in your React Native MacOS app, you probably just want to use OP-SQLite, which supports React Native MacOS out-of-the box.

I wanted to spin up a small personal app with React Native MacOS, an out-of-tree React Native platform. For this app, I wanted to use a local SQLite database for my application data. Specifically, I wanted to use Expo SQLite, because I love Expo.

Here's the problem though: Expo doesn't have first-class support for out-of-tree platforms (although some people are beginning to ask for it). I think it's totally understandable that Expo hasn't prioritized these platforms - there's a lot to manage!

Still, I wanted to see if I could figure something out for my personal project. After reading up on the out-of-tree docs, native modules/turbo modules documentation, and browsing some source code, I had an intuition that there was a path forward. As it happens, there's definitely a way to hack Expo SQLite into React Native MacOS. Not only can it be done, but learning how to do it taught me a lot about React Native, Expo, and building for Mac. If you're interested in tinkering with these things, read on for a step-by-step walkthrough. Here's what we'll cover:

  1. First, I'll walk through the current process of getting a React Native MacOS app set up.
  2. Once we have that project working, we'll install Expo modules with React Native MacOS, which will teach us some of the tricks to getting out-of-tree support for Expo modules (and other native modules more broadly), and give us the foundation we need to integrate Expo SQLite.
  3. Then we'll install expo-sqlite and hack it a little to work with React Native MacOS. It's a bit of a challenge, but totally doable if you're willing to get your hands dirty.

I will do my best to make this guide easy to follow for most audiences, but I'm going to assume readers have some degree of familiarity with JavaScript, React Native, and building apps for iOS (not a far cry from building for MacOS!). If you're looking for any clarity or have corrections/suggestions for me, reach out on Twitter.

If you'd like to see the finished code, I've published it in this GitHub repository

Ready? Let's go.

Set up React Native MacOS

Starting a React Native MacOS project is pretty straightforward. I was able to follow their docs and get a Mac app up and running without any issue. I'll copy the steps here to reduce open tabs on your browser:

First, set up a new React Native project with the React Native CLI:

npx @react-native-community/cli init macapp

We aren't doing anything fancy with this (yet), so accept any default prompts like installing CocoaPods.

Then, hop into the project directory:

cd macapp

From here, run the MacOS extension:

npx react-native-macos-init

That command will create a new directory at macapp/macos. This just a directory full of native files and configuration, similar to the standard android/ and ios/ directories created by the React Native CLI. When we need to make native code changes to the app, we'll end up in this folder.

The react-native-macos-init command will run pod install from inside that directory for you. Once the CocoaPods dependencies are installed, the init command will give you a few different options for next steps:

  Run instructions for macOS:
• npx react-native run-macos
- or -
• Open macos/macapp.xcworkspace in Xcode or run "xed -b macos"
yarn start:macos
• Hit the Run button

I suggest running:

yarn start # Do this in a separate terminal
npx react-native run-macos # Then run this in a second terminal from project root

This will start a Metro server and build the Mac app from the command line. After a little time, you should see something like this:

Image of a React native starter screen in a Mac app

There you have it: React Native MacOS up and running on your machine. Next, let's get Expo Modules integrated with our project.

Installing Expo Modules

In order to get to our end goal, using expo-sqlite, we need to install Expo modules. We'll have the best results with the manual installation instructions since we're using an unsupported platform. It will be easier for us to make our own adjustments in the manual process.

First, install expo from npm:

# npm
npm install expo
# yarn
yarn add expo

Android/iOS manual Expo installation (skipped)

You can follow the Android and iOS configuration here if you want to make your app cross-platform, but I'm going to skip those and jump to the Mac OS set up. For that, we'll follow some of the iOS configuration steps, with a slight modification.

MacOS manual Expo installation

Open up your AppDelegate.h file that was created by React Native Mac OS initialization. It should be somewhere like macos/macapp-macOS/AppDelegate.h. Mine looks like this by default:

#import <RCTAppDelegate.h>
#import <Cocoa/Cocoa.h>

@interface AppDelegate : RCTAppDelegate

@end

This looks a lot like what the iOS AppDelegate.h looks like for the Expo iOS instructions. With some trial and error, I found that we can add the Expo/Expo.h import, but if we use ExAppDelegateWrapper, the app stops rendering. I'm not exactly sure why, but we can leave the AppDelegate alone and the rest of this process will work. So update your Mac OS file to look like this:

#import <RCTAppDelegate.h>
#import <Expo/Expo.h> // Just add this line
#import <Cocoa/Cocoa.h>

@interface AppDelegate : RCTAppDelegate

@end

Next, take a look at the Mac OS Podfile at macos/Podfile. It'll probably look like:

require_relative '../node_modules/react-native-macos/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

prepare_react_native_project!

target 'macapp-macOS' do
platform :macos, '11.0'
use_native_modules!

# Flags change depending on the env values.
flags = get_default_flags()

use_react_native!(
:path => '../node_modules/react-native-macos',
:hermes_enabled => false,
:fabric_enabled => ENV['RCT_NEW_ARCH_ENABLED'] == '1',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)

post_install do |installer|
react_native_post_install(installer)
end
end

Again, this is pretty similar to the Podfile listed in the Expo iOS instructions. We can mostly copy those set up steps, although we will preserve some of the MacOS differences. We'll add the require statement and the post_integrate loop. Your final file should look like this:

require_relative '../node_modules/react-native-macos/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
# Add this to pull in Expo auto-linking.
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")

prepare_react_native_project!

target 'macapp-macOS' do
platform :macos, '11.0'
# Add this for Expo Modules
use_expo_modules!
post_integrate do |installer|
begin
expo_patch_react_imports!(installer)
rescue => e
Pod::UI.warn e
end
end
# End of addition for Expo Modules

use_native_modules!

# Flags change depending on the env values.
flags = get_default_flags()

use_react_native!(
:path => '../node_modules/react-native-macos',
:hermes_enabled => false,
:fabric_enabled => ENV['RCT_NEW_ARCH_ENABLED'] == '1',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)

post_install do |installer|
react_native_post_install(installer)
end
end

Once you've made these changes, hop into your macos directory and run another pod install:

cd macos
bundle exec pod install

Expo's auto-linking will discover any Expo modules that should get automatically installed with CocoaPods, and pull those over. Your output should contain some Expo packages, now!

# ... other output
Installing EXConstants (16.0.2)
Installing Expo (51.0.34)
Installing ExpoFileSystem (17.0.1)
Installing ExpoFont (12.0.10)
Installing ExpoKeepAwake (13.0.2)
Installing ExpoModulesCore (1.12.24)
# ... output continued

Since we've just made a native change to the Mac app, let's do a reality check and make sure things are still working. Open up your app with Metro and the react-native run-macos command:

yarn start # Do this in a separate terminal
npx react-native run-macos # Then run this in a second terminal from project root

If that's all lookin' good, then we can get to the fun part! Installing expo-sqlite and hacking it to work with Mac.

Installing Expo SQLite

This is where things get a little tricky, but it's also the fun part. I'm going to walk us through some of the major roadblocks and solutions I found while working through this on my own, which mostly involved experimentation and reading through GitHub comments for clues. For every step here, I'll attempt to demonstrate what I tried first, why it didn't work, and what the solution was.

Install Patch Package

We're going to be making some modifications to dependencies from npm, so we'll get a lot of use out of patch-package. Follow their set up guide so you can persist local changes to your dependencies as we modify them.

Basic Expo SQLite installation

The first thing we'll try is just running the Expo SQLite install step. The docs say it only supports Android and iOS, but it's always worth double checking these things yourself. Run:

npx expo install expo-sqlite
# You'll have to run another pod install,
# since Expo doesn't directly support the MacOS platform
cd macos && bundle exec pod install

Let's see if it Just Works by adding the demo code to App.tsx. I followed their first two code snippets. Import the module:

import * as SQLite from "expo-sqlite";

And then set up a useEffect with an async function to run the basic CRUD operations:

function App(): React.JSX.Element {
// ... rest of component here
useEffect(() => {
async function initDB() {
const db = await SQLite.openDatabaseAsync('databaseName');

// `execAsync()` is useful for bulk queries when you want to execute altogether.
// Please note that `execAsync()` does not escape parameters and may lead to SQL injection.
await db.execAsync(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL, intValue INTEGER);
INSERT INTO test (value, intValue) VALUES ('test1', 123);
INSERT INTO test (value, intValue) VALUES ('test2', 456);
INSERT INTO test (value, intValue) VALUES ('test3', 789);
`
);

// `runAsync()` is useful when you want to execute some write operations.
const result = await db.runAsync(
'INSERT INTO test (value, intValue) VALUES (?, ?)',
'aaa',
100,
);
console.log(result.lastInsertRowId, result.changes);
await db.runAsync(
'UPDATE test SET intValue = ? WHERE value = ?',
999,
'aaa',
); // Binding unnamed parameters from variadic arguments
await db.runAsync('UPDATE test SET intValue = ? WHERE value = ?', [
999,
'aaa',
]); // Binding unnamed parameters from array
await db.runAsync('DELETE FROM test WHERE value = $value', {
$value: 'aaa',
}); // Binding named parameters from object

// `getFirstAsync()` is useful when you want to get a single row from the database.
const firstRow = await db.getFirstAsync('SELECT * FROM test');
console.log(firstRow.id, firstRow.value, firstRow.intValue);

// `getAllAsync()` is useful when you want to get all results as an array of objects.
const allRows = await db.getAllAsync('SELECT * FROM test');
for (const row of allRows) {
console.log(row.id, row.value, row.intValue);
}

// `getEachAsync()` is useful when you want to iterate SQLite query cursor.
for await (const row of db.getEachAsync('SELECT * FROM test')) {
console.log(row.id, row.value, row.intValue);
}
}

initDB();
}, []);
// ... rest of component here

No dice - expo-sqlite does not run on Mac out of the box.

If you try running the Mac app with:

yarn start # Do this in a separate terminal
npx react-native run-macos # Then run this in a second terminal from project root

The native app will build successfully, but you'll get an error in Metro:

 ERROR  Error: Cannot find native module 'ExpoSQLiteNext'
LOG Running "macapp" with {"rootTag":1,"initialProps":{"concurrentRoot":false}}
ERROR Invariant Violation: "macapp" has not been registered. This can happen if:
* Metro (the local dev server) is run from the wrong folder. Check if Metro is running, stop it and restart it in the current project.
* A module failed to load due to an error and `AppRegistry.registerComponent` wasn't called.

This makes sense - the Expo SQLite podspec doesn't include osx in its platforms key, so even though we were able to install the npm package and even run CocoaPods successfully, it didn't actually pull anything over into the native project. If you want to see what I mean by this, you can re-run CocoaPods from scratch:

cd macos
# Remove Pods so you can see the full output of pod install
rm -rf Pods/
bundle exec pod install

Look for the dependencies output, which looks kind of like this:

# ... other output
Downloading dependencies
Installing DoubleConversion (1.1.6)
Installing EXConstants (16.0.2)
Installing Expo (51.0.34)
Installing ExpoFileSystem (17.0.1)
Installing ExpoFont (12.0.10)
Installing ExpoKeepAwake (13.0.2)
Installing ExpoModulesCore (1.12.24)
# ... and so on

Notice we see some Expo packages, but not Expo SQLite. We need to find a way to get the Expo SQLite package to install itself during the CocoaPods installation when run from the macos project.

How can we add platforms to Expo SQLite?

At this point, I benefited from serendipity. I had been listening to the Rocket Ship Podcast episode with Oscar Franco before working on this, and Oscar Mentioned his package op-sqlite. As I mentioned at the beginning, this is probably the best package to use off-the-shelf, since it supports MacOS by default.

I figured if op-sqlite worked with MacOS, and expo-sqlite doesn't, I should compare the two packages to see what's different. I started with the podspecs and the crucial details began to emerge. Check it out:

  1. expo-sqlite podspec
  2. op-sqlite podspec

I noticed that where expo-sqlite only lists ios in its platform config, op-sqlite lists multiple. Specifically:

ExpoSQLite

  # ExpoSQLite.podspec
s.platform = :ios, '13.4'

OP-SQLite

  # op-sqlite.podspec
s.platforms = { :ios => "13.0", :osx => "10.15", :visionos => "1.0" }

Patching expo-sqlite to run on MacOS

So what would happen if we modified our local node_modules/expo-sqlite/ios/ExpoSQLite.podspec to look like:

require 'json'

package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
podfile_properties = JSON.parse(File.read("#{Pod::Config.instance.installation_root}/Podfile.properties.json")) rescue {}

sqliteVersion = '3.45.3+1'

Pod::Spec.new do |s|
s.name = 'ExpoSQLite'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platforms = { :ios => "13.4", :osx => "11.0" } # Change this line, and use "11.0" since that's what we have in `macos/Podfile`
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true
s.dependency 'ExpoModulesCore'

s.dependency 'sqlite3', "~> #{sqliteVersion}"
s.dependency 'sqlite3/bytecodevtab', "~> #{sqliteVersion}"
unless podfile_properties['expo.sqlite.enableFTS'] === 'false'
s.dependency 'sqlite3/fts', "~> #{sqliteVersion}"
s.dependency 'sqlite3/fts5', "~> #{sqliteVersion}"
end

# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
}

s.source_files = "**/*.{h,m,swift}"
s.vendored_frameworks = 'crsqlite.xcframework'
end

If you go into your node_modules/ folder and make that change, you can persist it by running:

# Make sure to run this from the root of the project
yarn patch-package expo-sqlite

With our patch in place, let's see what happens with a new CocoaPods installation:

cd macos && bundle exec pod install

Hey look at that! You should now see this in the output:

# ... other stuff
Downloading dependencies
Installing ExpoSQLite (14.0.6)
Installing sqlite3 (3.45.3+1)
Generating Pods project
# ... other stuff

That's promising! Let's build the app again.

yarn start # Do this in a separate terminal
npx react-native run-macos # Then run this in a second terminal from project root

Ah dang, this time it actually doesn't build at all! That's annoying.

A new error message has appeared

You might think we've regressed. But actually, this new error is a sign of progress. The output from the CLI will be pretty long, so I recommend opening the project up in Xcode. To do that, open your Xcode, and select open an existing project, and then choose the workspace at macos/macapp.xcworkspace.

Once Xcode is up and running, hit the play button to build to your local machine. Xcode will give you a much easier to read error:

Xcode error that reads 'Framework crsqlite not found'

Searching for CRSQLite clues

This is a tough one. I tried searching for Framework 'crsqlite' not found, but that specific error doesn't crop up much in search engines. If you just search for crsqlite, you'll find the project on GitHub. It looks cool! Originally, I thought maybe it was quite important to Expo SQLite. So I figured I'd search the expo-sqlite codebase for mentions of it to learn more and hopefully find some clues.

Searching the codebase brings us back to the ExpoSQLite.podspec file, where crsqlite is included in the vendored_frameworks property:

  # ExpoSQLite.podspec
s.vendored_frameworks = 'crsqlite.xcframework'

If you check the file tree view in GitHub, you'll see this framework sits inside the package itself. In the git history, there's a PR that adds suport for CRSQLite on Android. I looked at the changes and got the sense that to add a new platform, I'd have to figure out how to build CRSQLite for Mac targets and include it as a framework. At least, that was my first intuition.

Since I've never worked with that project before, I decided to check the GitHub discussions in Expo. I searched for crsqlite and found something interesting: it looks like the Expo team is considering removing CRSQLite. That's a very helpful tidbit. Instead of figuring out how to include this package for Mac, we can flip the task on its head and figure out how to remove CRSQLite.

Removing CRSQLite from Expo SQLite

I took a stab at the simplest possible plan: delete anything that references the framework. Here's what I did:

  1. Delete s.vendored_frameworks = 'crsqlite.xcframework' from node_modules/expo-sqlite/ios/ExpoSQLite.podspec
  2. Delete node_modules/expo-sqlite/ios/crsqlite.xcframework entirely
  3. Delete node_modules/expo-sqlite/ios/CRSQLiteLoader.h and node_modules/expo-sqlite/ios/CRSQLiteLoader.m
  4. Did a search in the codebase for crsqlite and remove all instances of those references, except for the config in node_modules/expo-sqlite/ios/SQLiteOptions.swift, which seemed like it would just evaluate to false and do nothing.
  5. Ran patch-package again from the root of the project with yarn patch-package expo-sqlite

This will generate a patch file that looks like this:

diff --git a/node_modules/expo-sqlite/ios/CRSQLiteLoader.h b/node_modules/expo-sqlite/ios/CRSQLiteLoader.h
deleted file mode 100644
index 552a838..0000000
--- a/node_modules/expo-sqlite/ios/CRSQLiteLoader.h
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#pragma once
-
-struct sqlite3;
-
-int crsqlite_init_from_swift(struct sqlite3 *db);
diff --git a/node_modules/expo-sqlite/ios/CRSQLiteLoader.m b/node_modules/expo-sqlite/ios/CRSQLiteLoader.m
deleted file mode 100644
index 7e60d35..0000000
--- a/node_modules/expo-sqlite/ios/CRSQLiteLoader.m
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#import <ExpoSQLite/CRSQLiteLoader.h>
-#import <sqlite3/sqlite3.h>
-
-int crsqlite_init_from_swift(sqlite3 *db) {
- sqlite3_enable_load_extension(db, 1);
- char *errorMessage;
- NSBundle *bundle = [NSBundle bundleWithIdentifier:@"io.vlcn.crsqlite"];
- NSString *libPath = [bundle pathForResource:@"crsqlite" ofType:@""];
- int result = sqlite3_load_extension(db, [libPath UTF8String], "sqlite3_crsqlite_init", &errorMessage);
- if (result != SQLITE_OK) {
- NSLog(@"Failed to load sqlite3 extension: %@", [NSString stringWithCString:errorMessage]);
- sqlite3_free(errorMessage);
- errorMessage = nil;
- }
- return result;
-}
diff --git a/node_modules/expo-sqlite/ios/ExpoSQLite.podspec b/node_modules/expo-sqlite/ios/ExpoSQLite.podspec
index 870d427..508d6fe 100644
--- a/node_modules/expo-sqlite/ios/ExpoSQLite.podspec
+++ b/node_modules/expo-sqlite/ios/ExpoSQLite.podspec
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
- s.platform = :ios, '13.4'
+ s.platforms = { :ios => "13.4", :osx => "11.0" } # Change this line, and use "11.0" since that's what we have in `macos/Podfile`
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
@@ -31,5 +31,4 @@ Pod::Spec.new do |s|
}

s.source_files = "**/*.{h,m,swift}"
- s.vendored_frameworks = 'crsqlite.xcframework'
end
diff --git a/node_modules/expo-sqlite/ios/SQLiteModuleNext.swift b/node_modules/expo-sqlite/ios/SQLiteModuleNext.swift
index 66bbb6c..381bb36 100644
--- a/node_modules/expo-sqlite/ios/SQLiteModuleNext.swift
+++ b/node_modules/expo-sqlite/ios/SQLiteModuleNext.swift
@@ -230,9 +230,6 @@ public final class SQLiteModuleNext: Module {

private func initDb(database: NativeDatabase) throws {
try maybeThrowForClosedDatabase(database)
- if database.openOptions.enableCRSQLite {
- crsqlite_init_from_swift(database.pointer)
- }
if database.openOptions.enableChangeListener {
addUpdateHook(database)
}
@@ -371,9 +368,6 @@ public final class SQLiteModuleNext: Module {
sqlite3_finalize(removedStatement.pointer)
}

- if db.openOptions.enableCRSQLite {
- sqlite3_exec(db.pointer, "SELECT crsql_finalize()", nil, nil, nil)
- }
let ret = sqlite3_close(db.pointer)
db.isClosed = true

diff --git a/node_modules/expo-sqlite/ios/crsqlite.xcframework/Info.plist b/node_modules/expo-sqlite/ios/crsqlite.xcframework/Info.plist
deleted file mode 100644
index f4e8031..0000000
--- a/node_modules/expo-sqlite/ios/crsqlite.xcframework/Info.plist
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>AvailableLibraries</key>
- <array>
- <dict>
- <key>LibraryIdentifier</key>
- <string>ios-arm64_x86_64-simulator</string>
- <key>LibraryPath</key>
- <string>crsqlite.framework</string>
- <key>SupportedArchitectures</key>
- <array>
- <string>arm64</string>
- <string>x86_64</string>
- </array>
- <key>SupportedPlatform</key>
- <string>ios</string>
- <key>SupportedPlatformVariant</key>
- <string>simulator</string>
- </dict>
- <dict>
- <key>LibraryIdentifier</key>
- <string>ios-arm64</string>
- <key>LibraryPath</key>
- <string>crsqlite.framework</string>
- <key>SupportedArchitectures</key>
- <array>
- <string>arm64</string>
- </array>
- <key>SupportedPlatform</key>
- <string>ios</string>
- </dict>
- </array>
- <key>CFBundlePackageType</key>
- <string>XFWK</string>
- <key>XCFrameworkFormatVersion</key>
- <string>1.0</string>
- <key>CFBundleVersion</key>
- <string>1</string>
- <key>CFBundleShortVersionString</key>
- <string>1.0.0</string>
-</dict>
-</plist>
diff --git a/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64/crsqlite.framework/Info.plist b/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64/crsqlite.framework/Info.plist
deleted file mode 100644
index 9d5560e..0000000
--- a/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64/crsqlite.framework/Info.plist
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>CFBundleDevelopmentRegion</key>
- <string>en</string>
- <key>CFBundleExecutable</key>
- <string>crsqlite</string>
- <key>CFBundleIdentifier</key>
- <string>io.vlcn.crsqlite</string>
- <key>CFBundleInfoDictionaryVersion</key>
- <string>6.0</string>
- <key>CFBundleVersion</key>
- <string>1</string>
- <key>CFBundleShortVersionString</key>
- <string>1.0.0</string>
- <key>CFBundlePackageType</key>
- <string>FMWK</string>
- <key>CFBundleSignature</key>
- <string>????</string>
- <key>MinimumOSVersion</key>
- <string>8.0</string>
-</dict>
-</plist>
diff --git a/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64/crsqlite.framework/crsqlite b/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64/crsqlite.framework/crsqlite
deleted file mode 100755
index bd379bc..0000000
Binary files a/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64/crsqlite.framework/crsqlite and /dev/null differ
diff --git a/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64_x86_64-simulator/crsqlite.framework/Info.plist b/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64_x86_64-simulator/crsqlite.framework/Info.plist
deleted file mode 100644
index e3fbebf..0000000
--- a/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64_x86_64-simulator/crsqlite.framework/Info.plist
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>CFBundleDevelopmentRegion</key>
- <string>en</string>
- <key>CFBundleExecutable</key>
- <string>crsqlite</string>
- <key>CFBundleIdentifier</key>
- <string>io.vlcn.crsqlite</string>
- <key>CFBundleInfoDictionaryVersion</key>
- <string>6.0</string>
- <key>CFBundlePackageType</key>
- <string>FMWK</string>
- <key>CFBundleSignature</key>
- <string>????</string>
- <key>MinimumOSVersion</key>
- <string>8.0</string>
-</dict>
-</plist>
diff --git a/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64_x86_64-simulator/crsqlite.framework/crsqlite b/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64_x86_64-simulator/crsqlite.framework/crsqlite
deleted file mode 100755
index 1f76709..0000000
Binary files a/node_modules/expo-sqlite/ios/crsqlite.xcframework/ios-arm64_x86_64-simulator/crsqlite.framework/crsqlite and /dev/null differ

With that in place, let's run the app and see what happens:

yarn start --reset-cache # Do this in a separate terminal, use --reset-cache for good measure
npx react-native run-macos # Then run this in a second terminal from project root

Hey, it works on my machine!

Yeehaw, it works! Your application may ask for permissions to write to disk, which makes sense, since the SQLite database is just a file on disk. Accept the permissions request, and you'll see:

React Native MacOS with Expo SQLite running

Caveats

When I did this the first time, I ran into another issue with an Expo module not loading:

 ERROR  Error: Cannot find native module 'ExpoAsset'

I patched expo-asset like this:

diff --git a/node_modules/expo-asset/ios/ExpoAsset.podspec b/node_modules/expo-asset/ios/ExpoAsset.podspec
index df1e3c1..247e762 100644
--- a/node_modules/expo-asset/ios/ExpoAsset.podspec
+++ b/node_modules/expo-asset/ios/ExpoAsset.podspec
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
- s.platforms = { :ios => '13.4', :tvos => '13.4' }
+ s.platforms = { :ios => '13.4', :tvos => '13.4', :osx => '11.0'}
s.swift_version = '5.4'
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true

That was the last change I needed to make. But when I sat down to write this blog post, I didn't encounter that error. I'm not exactly sure why that happened, I assume there was a small update in the intervening time, or I did something slightly differently the first time that I side-stepped when I built this public demo. Either way, you may run into similar problems.

This approach is definitely something to use at your own risk, since it's not officially supported by any of the packages. Still, I think there's a lot of value to tinkering with these internals. It de-mystifies a lot of the native code in React Native, and if you get comfortable with these kinds of hacks, you add more troubleshooting and experimental capabilities to your personal skillset. I hope following along this blog post has been interesting, informative, and enjoyable for you!

Conclusion

Again, I don't recommend running this code in any business-critical production applications. For me, I had a small personal app I wanted to build, and solving this challenge was a good way for me to learn about React Native internals and out-of-tree platforms. I'm excited to use Expo SQLite in my personal app, and hopefully this post helps other people explore, experiment, and move the platform forward!

As a reminder, if you'd like to see the finished code, I've published it in this GitHub repository. It still has some TypeScript errors in the demo SQLite code, but I'll leave those as an exercise for the reader to solve or remove.

Thanks for reading!

Update: 2024-10-26

I took this initial work and wrote a PR for Expo, which was recently merged. So if you'd like to use Expo SQLite in your React Native for Mac app, you may soon be able to do so without patching the package yourself.