diff --git a/.changeset/fix-capacitor-batch-operations.md b/.changeset/fix-capacitor-batch-operations.md new file mode 100644 index 000000000..571175c40 --- /dev/null +++ b/.changeset/fix-capacitor-batch-operations.md @@ -0,0 +1,5 @@ +--- +'@powersync/capacitor': patch +--- + +Fix Capacitor batch operations so they do not start a nested native transaction when executed inside PowerSync's write transaction wrapper. diff --git a/.github/actions/setup-android-emulator/action.yaml b/.github/actions/setup-android-emulator/action.yaml new file mode 100644 index 000000000..087d65f0a --- /dev/null +++ b/.github/actions/setup-android-emulator/action.yaml @@ -0,0 +1,70 @@ +name: Setup Android Emulator +description: Configure KVM, Java, Gradle, and a cached Android emulator snapshot. +inputs: + avd-name: + description: Name of the Android Virtual Device to create or reuse. + required: true + cache-key: + description: Cache key for the Android Virtual Device files. + required: true + api-level: + description: Android API level for the emulator. + required: false + default: '31' + target: + description: Android system image target. + required: false + default: google_apis + arch: + description: Android system image architecture. + required: false + default: x86_64 + java-version: + description: JDK version to install for Android builds. + required: false + default: '17' +runs: + using: composite + steps: + - name: Enable KVM group perms + shell: bash + 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: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: AVD Cache + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: ${{ inputs.cache-key }} + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: ${{ inputs.java-version }} + distribution: adopt + cache: gradle + + - name: Initialize Android Folder + shell: bash + run: mkdir -p ~/.android/avd + + - name: Create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2.28.0 + with: + api-level: ${{ inputs.api-level }} + force-avd-creation: false + target: ${{ inputs.target }} + arch: ${{ inputs.arch }} + disable-animations: false + avd-name: ${{ inputs.avd-name }} + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: echo "Generated AVD snapshot for caching." diff --git a/.github/workflows/test-simulators.yaml b/.github/workflows/test-simulators.yaml index ae3421aba..8768eb76e 100644 --- a/.github/workflows/test-simulators.yaml +++ b/.github/workflows/test-simulators.yaml @@ -3,12 +3,16 @@ name: Test Simulators/Emulators on: pull_request: # triggered for any PR updates (including new pushes to PR branch) +permissions: + contents: read + jobs: check-changes: name: Check for relevant changes runs-on: ubuntu-latest outputs: - should_run: ${{ steps.check.outputs.should_run }} + react_native_simulator_should_run: ${{ steps.check.outputs.react_native_simulator_should_run }} + capacitor_should_run: ${{ steps.check.outputs.capacitor_should_run }} steps: - uses: actions/checkout@v6 with: @@ -18,15 +22,21 @@ jobs: run: | git fetch origin ${{ github.base_ref }} if git diff --quiet origin/${{ github.base_ref }} -- packages/common packages/powersync-op-sqlite tools/powersynctests; then - echo "should_run=false" >> $GITHUB_OUTPUT + echo "react_native_simulator_should_run=false" >> $GITHUB_OUTPUT + else + echo "react_native_simulator_should_run=true" >> $GITHUB_OUTPUT + fi + + if git diff --quiet origin/${{ github.base_ref }} -- packages/capacitor pnpm-lock.yaml .github/workflows/test-simulators.yaml; then + echo "capacitor_should_run=false" >> $GITHUB_OUTPUT else - echo "should_run=true" >> $GITHUB_OUTPUT + echo "capacitor_should_run=true" >> $GITHUB_OUTPUT fi test-android: name: Test Android needs: check-changes - if: ${{ needs.check-changes.outputs.should_run == 'true' }} + if: ${{ needs.check-changes.outputs.react_native_simulator_should_run == 'true' }} runs-on: ubuntu-xl timeout-minutes: 30 env: @@ -36,30 +46,11 @@ jobs: with: persist-credentials: false - - name: Enable KVM group perms - 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: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: AVD Cache - uses: actions/cache@v3 - id: avd-cache + - name: Setup Android emulator + uses: ./.github/actions/setup-android-emulator with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-31 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: 'adopt' - cache: 'gradle' + avd-name: ${{ env.AVD_NAME }} + cache-key: avd-31 - name: Enable Corepack run: corepack enable @@ -80,22 +71,6 @@ jobs: run: | pnpx detox clean-framework-cache && pnpx detox build-framework-cache - - name: Initialize Android Folder - run: mkdir -p ~/.android/avd - - - name: create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2.28.0 - with: - api-level: 31 - force-avd-creation: false - target: google_apis - arch: x86_64 - disable-animations: false - avd-name: $AVD_NAME - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - script: echo "Generated AVD snapshot for caching." - - name: Android Emulator Build working-directory: ./tools/powersynctests run: pnpx detox build --configuration android.emu.release @@ -106,18 +81,84 @@ jobs: api-level: 31 target: google_apis arch: x86_64 - avd-name: $AVD_NAME + avd-name: ${{ env.AVD_NAME }} script: cd tools/powersynctests && pnpx detox test --configuration android.emu.release --headless force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true + test-capacitor-android: + name: Test Capacitor Android + needs: check-changes + if: ${{ needs.check-changes.outputs.capacitor_should_run == 'true' }} + runs-on: ubuntu-xl + timeout-minutes: 30 + env: + AVD_NAME: capacitor-avd-x86_64-35 + TEST_PLATFORM: android + TEST_TARGET: emulator-5554 + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup Android emulator + uses: ./.github/actions/setup-android-emulator + with: + avd-name: ${{ env.AVD_NAME }} + cache-key: capacitor-avd-35 + api-level: '35' + java-version: '21' + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + cache: pnpm + + - name: Install dependencies + run: pnpm install + + - name: Build + run: pnpm build:packages + + - name: Install Capacitor example dependencies + working-directory: ./packages/capacitor/example-app + run: pnpm install + + - name: Sync and build Capacitor Android app + working-directory: ./packages/capacitor/example-app + # Prebuild the native app so the later browser-test action can start quickly. + # This dummy Vitest server URL is only used for the prebuild; the test run + # updates Capacitor with the actual Vitest URL before launching the app. + env: + CAPACITOR_VITEST_SERVER_URL: http://10.0.2.2 + run: | + pnpm exec cap sync android + cd android + ./gradlew assembleDebug + + - name: Run Capacitor Android browser tests + uses: reactivecircus/android-emulator-runner@v2.28.0 + with: + api-level: 35 + target: google_apis + arch: x86_64 + avd-name: ${{ env.AVD_NAME }} + script: pnpm --dir packages/capacitor exec vitest run --config vitest.config.ts + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + test-ios: name: Test iOS needs: check-changes # TODO: Re-enable iOS tests. They have been disabled because they are failing extremely frequently without # any apparent cause. In particular, it seems like even starting the simulator times out most of the time. - if: ${{ false && needs.check-changes.outputs.should_run == 'true' }} + if: ${{ false && needs.check-changes.outputs.react_native_simulator_should_run == 'true' }} runs-on: macOS-15 timeout-minutes: 30 diff --git a/demos/example-capacitor/ios/App/CapApp-SPM/Package.resolved b/demos/example-capacitor/ios/App/CapApp-SPM/Package.resolved new file mode 100644 index 000000000..c7d0d211c --- /dev/null +++ b/demos/example-capacitor/ios/App/CapApp-SPM/Package.resolved @@ -0,0 +1,41 @@ +{ + "pins" : [ + { + "identity" : "capacitor-swift-pm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ionic-team/capacitor-swift-pm.git", + "state" : { + "branch" : "8.0.0", + "revision" : "596259033e94829dffc552a40e7129262122995e" + } + }, + { + "identity" : "powersync-sqlite-core-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + "state" : { + "revision" : "8571d46d169a24ee1f3733cb554b23b9408cbe09", + "version" : "0.4.14" + } + }, + { + "identity" : "sqlcipher.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sqlcipher/SQLCipher.swift.git", + "state" : { + "revision" : "07bf6bc2191a063d6f1e7c3b5f276a3fadfe36b7", + "version" : "4.16.0" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation.git", + "state" : { + "revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d", + "version" : "0.9.20" + } + } + ], + "version" : 2 +} diff --git a/packages/capacitor/DEVELOP.md b/packages/capacitor/DEVELOP.md new file mode 100644 index 000000000..524ec4234 --- /dev/null +++ b/packages/capacitor/DEVELOP.md @@ -0,0 +1,67 @@ +# Developing the Capacitor SDK + +## Native browser integration tests + +The Capacitor package uses Vitest browser mode with a custom provider. Vitest starts a browser test server, then the provider runs the example Capacitor app and points the app at that server. + +Build the workspace before running these tests so the Capacitor package can resolve generated workspace package outputs: + +```sh +pnpm install +pnpm build:packages +cd packages/capacitor/example-app +pnpm install +cd .. +``` + +The test provider reads these environment variables: + +- `TEST_PLATFORM`: Native platform to run. Use `ios` or `android`. +- `TEST_TARGET`: Simulator/emulator target id passed to `cap run --target`. +- `TEST_SERVER_HOST`: Hostname the native app should use to reach the Vitest server. Android defaults to `10.0.2.2`; iOS uses the Vitest URL host as-is. + +### iOS + +List available iOS simulator targets: + +```sh +cd packages/capacitor/example-app +pnpm exec cap run ios --list +``` + +Run the integration tests on a simulator: + +```sh +cd packages/capacitor +TEST_PLATFORM=ios \ +TEST_TARGET= \ +pnpm exec vitest run --config vitest.config.ts +``` + +### Android + +Create the Android example app platform if it is not present yet: + +```sh +cd packages/capacitor/example-app +pnpm exec cap add android +``` + +List available Android emulator/device targets: + +```sh +adb devices +``` + +For the default Android emulator, the target is usually `emulator-5554`. + +Run the integration tests on Android: + +```sh +cd packages/capacitor +TEST_PLATFORM=android \ +TEST_TARGET=emulator-5554 \ +pnpm exec vitest run --config vitest.config.ts +``` + +Android defaults `TEST_SERVER_HOST` to `10.0.2.2`, which lets the emulator reach the host machine without `adb reverse`. The Vitest config also binds the test server to `0.0.0.0` for this reason. diff --git a/packages/capacitor/example-app/android/.gitignore b/packages/capacitor/example-app/android/.gitignore new file mode 100644 index 000000000..48354a3df --- /dev/null +++ b/packages/capacitor/example-app/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/packages/capacitor/example-app/android/app/.gitignore b/packages/capacitor/example-app/android/app/.gitignore new file mode 100644 index 000000000..043df802a --- /dev/null +++ b/packages/capacitor/example-app/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/packages/capacitor/example-app/android/app/build.gradle b/packages/capacitor/example-app/android/app/build.gradle new file mode 100644 index 000000000..50107dc20 --- /dev/null +++ b/packages/capacitor/example-app/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace = "com.powersync.capacitor" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "com.powersync.capacitor" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/packages/capacitor/example-app/android/app/capacitor.build.gradle b/packages/capacitor/example-app/android/app/capacitor.build.gradle new file mode 100644 index 000000000..a97547ab9 --- /dev/null +++ b/packages/capacitor/example-app/android/app/capacitor.build.gradle @@ -0,0 +1,20 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + implementation project(':powersync-capacitor') + implementation project(':capacitor-community-sqlite') + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/packages/capacitor/example-app/android/app/proguard-rules.pro b/packages/capacitor/example-app/android/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/packages/capacitor/example-app/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/packages/capacitor/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/packages/capacitor/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 000000000..f2c2217ef --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/packages/capacitor/example-app/android/app/src/main/AndroidManifest.xml b/packages/capacitor/example-app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4e182ed07 --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/capacitor/example-app/android/app/src/main/java/com/powersync/capacitor/MainActivity.java b/packages/capacitor/example-app/android/app/src/main/java/com/powersync/capacitor/MainActivity.java new file mode 100644 index 000000000..124e9036b --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/java/com/powersync/capacitor/MainActivity.java @@ -0,0 +1,5 @@ +package com.powersync.capacitor; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity {} diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 000000000..e31573b4f Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 000000000..f7a64923e Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 000000000..807725501 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 000000000..14c6c8fe3 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 000000000..244ca2506 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 000000000..74faaa583 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 000000000..e944f4ad4 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 000000000..564a82ff9 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 000000000..bfabe6871 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 000000000..692907126 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/packages/capacitor/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..c7bd21dbd --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml b/packages/capacitor/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..d5fccc538 --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/capacitor/example-app/android/app/src/main/res/drawable/splash.png b/packages/capacitor/example-app/android/app/src/main/res/drawable/splash.png new file mode 100644 index 000000000..f7a64923e Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/drawable/splash.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/layout/activity_main.xml b/packages/capacitor/example-app/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..b5ad13870 --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/capacitor/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/packages/capacitor/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/packages/capacitor/example-app/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/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..c023e5059 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..2127973b2 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..b441f37d6 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..72905b854 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..8ed0605c2 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..9502e47a2 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..4d1e07710 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..df0f15880 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..853db043d Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..6cdf97c11 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..2960cbb61 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..8e3093a86 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..46de6e255 Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..d2ea9abed Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..a40d73e9c Binary files /dev/null and b/packages/capacitor/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/packages/capacitor/example-app/android/app/src/main/res/values/ic_launcher_background.xml b/packages/capacitor/example-app/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..c5d5899fd --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/packages/capacitor/example-app/android/app/src/main/res/values/strings.xml b/packages/capacitor/example-app/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..c8edf1a78 --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + PowerSync Capacitor SDK example + PowerSync Capacitor SDK example + com.powersync.capacitor + com.powersync.capacitor + diff --git a/packages/capacitor/example-app/android/app/src/main/res/values/styles.xml b/packages/capacitor/example-app/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..be874e54a --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/capacitor/example-app/android/app/src/main/res/xml/file_paths.xml b/packages/capacitor/example-app/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..bd0c4d80d --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/capacitor/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/packages/capacitor/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 000000000..029732784 --- /dev/null +++ b/packages/capacitor/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/packages/capacitor/example-app/android/build.gradle b/packages/capacitor/example-app/android/build.gradle new file mode 100644 index 000000000..f8f0e43b6 --- /dev/null +++ b/packages/capacitor/example-app/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/capacitor/example-app/android/capacitor.settings.gradle b/packages/capacitor/example-app/android/capacitor.settings.gradle new file mode 100644 index 000000000..4cfa7f0aa --- /dev/null +++ b/packages/capacitor/example-app/android/capacitor.settings.gradle @@ -0,0 +1,9 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/android/capacitor') + +include ':powersync-capacitor' +project(':powersync-capacitor').projectDir = new File('../node_modules/@powersync/capacitor/android') + +include ':capacitor-community-sqlite' +project(':capacitor-community-sqlite').projectDir = new File('../node_modules/.pnpm/@capacitor-community+sqlite@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor-community/sqlite/android') diff --git a/packages/capacitor/example-app/android/gradle.properties b/packages/capacitor/example-app/android/gradle.properties new file mode 100644 index 000000000..2e87c52f8 --- /dev/null +++ b/packages/capacitor/example-app/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/packages/capacitor/example-app/android/gradle/wrapper/gradle-wrapper.jar b/packages/capacitor/example-app/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..1b33c55ba Binary files /dev/null and b/packages/capacitor/example-app/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/capacitor/example-app/android/gradle/wrapper/gradle-wrapper.properties b/packages/capacitor/example-app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..7705927e9 --- /dev/null +++ b/packages/capacitor/example-app/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/capacitor/example-app/android/gradlew b/packages/capacitor/example-app/android/gradlew new file mode 100755 index 000000000..23d15a936 --- /dev/null +++ b/packages/capacitor/example-app/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/packages/capacitor/example-app/android/gradlew.bat b/packages/capacitor/example-app/android/gradlew.bat new file mode 100644 index 000000000..db3a6ac20 --- /dev/null +++ b/packages/capacitor/example-app/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/capacitor/example-app/android/settings.gradle b/packages/capacitor/example-app/android/settings.gradle new file mode 100644 index 000000000..3b4431d77 --- /dev/null +++ b/packages/capacitor/example-app/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/packages/capacitor/example-app/android/variables.gradle b/packages/capacitor/example-app/android/variables.gradle new file mode 100644 index 000000000..ee4ba41c4 --- /dev/null +++ b/packages/capacitor/example-app/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} \ No newline at end of file diff --git a/packages/capacitor/example-app/capacitor.config.json b/packages/capacitor/example-app/capacitor.config.json deleted file mode 100644 index bab38af98..000000000 --- a/packages/capacitor/example-app/capacitor.config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "appId": "com.example.plugin", - "appName": "example-app", - "webDir": "dist", - "plugins": { - "SplashScreen": { - "launchAutoHide": false - } - } -} \ No newline at end of file diff --git a/packages/capacitor/example-app/capacitor.config.ts b/packages/capacitor/example-app/capacitor.config.ts new file mode 100644 index 000000000..4365b5dfa --- /dev/null +++ b/packages/capacitor/example-app/capacitor.config.ts @@ -0,0 +1,17 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'com.powersync.capacitor', + appName: 'PowerSync Capacitor SDK example', + // Native Vitest runs load the test server URL directly, so use an existing placeholder directory instead of requiring a web build. + webDir: process.env.CAPACITOR_VITEST_SERVER_URL ? 'src' : 'dist', + server: { + cleartext: true, + /** + * We receive the Vitest URL as an environment variable, Capacitor should load this on boot. + */ + url: process.env.CAPACITOR_VITEST_SERVER_URL + } +}; + +export default config; diff --git a/packages/capacitor/example-app/ios/App/CapApp-SPM/Package.swift b/packages/capacitor/example-app/ios/App/CapApp-SPM/Package.swift index eacaf5981..71c967fd0 100644 --- a/packages/capacitor/example-app/ios/App/CapApp-SPM/Package.swift +++ b/packages/capacitor/example-app/ios/App/CapApp-SPM/Package.swift @@ -12,7 +12,12 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.3.4"), - .package(name: "PowersyncCapacitor", path: "../../../node_modules/@powersync/capacitor") + .package(name: "PowersyncCapacitor", path: "../../../node_modules/@powersync/capacitor"), + .package( + name: "CapacitorCommunitySqlite", + path: + "../../../node_modules/.pnpm/@capacitor-community+sqlite@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor-community/sqlite" + ), ], targets: [ .target( @@ -20,7 +25,8 @@ let package = Package( dependencies: [ .product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm"), - .product(name: "PowersyncCapacitor", package: "PowersyncCapacitor") + .product(name: "PowersyncCapacitor", package: "PowersyncCapacitor"), + .product(name: "CapacitorCommunitySqlite", package: "CapacitorCommunitySqlite"), ] ) ] diff --git a/packages/capacitor/example-app/package.json b/packages/capacitor/example-app/package.json index 44934d2af..b5013210b 100644 --- a/packages/capacitor/example-app/package.json +++ b/packages/capacitor/example-app/package.json @@ -16,7 +16,8 @@ "@capacitor/core": "^8.0.0", "@powersync/capacitor": "file:..", "@capacitor/ios": "^8.0.0", - "@capacitor/android": "^8.0.0" + "@capacitor/android": "^8.0.0", + "@capacitor-community/sqlite": "~8.1.0" }, "devDependencies": { "@capacitor/cli": "^8.0.0", diff --git a/packages/capacitor/example-app/pnpm-lock.yaml b/packages/capacitor/example-app/pnpm-lock.yaml index e5ab6b44a..dbc9eccc8 100644 --- a/packages/capacitor/example-app/pnpm-lock.yaml +++ b/packages/capacitor/example-app/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@capacitor-community/sqlite': + specifier: ~8.1.0 + version: 8.1.0(@capacitor/core@8.3.4) '@capacitor/android': specifier: ^8.0.0 version: 8.3.4(@capacitor/core@8.3.4) @@ -19,7 +22,7 @@ importers: version: 8.3.4(@capacitor/core@8.3.4) '@powersync/capacitor': specifier: file:.. - version: file:..(@capacitor-community/sqlite@8.1.0(@capacitor/core@8.3.4)) + version: file:..(@capacitor-community/sqlite@8.1.0(@capacitor/core@8.3.4))(@capacitor/core@8.3.4) devDependencies: '@capacitor/cli': specifier: ^8.0.0 @@ -112,6 +115,7 @@ packages: resolution: {directory: .., type: directory} peerDependencies: '@capacitor-community/sqlite': ^8.1.0 + '@capacitor/core': '>=8.0.0' '@powersync/web': workspace:^1.38.0 '@rolldown/binding-android-arm64@1.0.1': @@ -931,9 +935,10 @@ snapshots: '@oxc-project/types@0.130.0': {} - '@powersync/capacitor@file:..(@capacitor-community/sqlite@8.1.0(@capacitor/core@8.3.4))': + '@powersync/capacitor@file:..(@capacitor-community/sqlite@8.1.0(@capacitor/core@8.3.4))(@capacitor/core@8.3.4)': dependencies: '@capacitor-community/sqlite': 8.1.0(@capacitor/core@8.3.4) + '@capacitor/core': 8.3.4 '@rolldown/binding-android-arm64@1.0.1': optional: true diff --git a/packages/capacitor/ios/capacitor-cordova-ios-plugins/CordovaPluginsResources.podspec b/packages/capacitor/ios/capacitor-cordova-ios-plugins/CordovaPluginsResources.podspec new file mode 100644 index 000000000..fa16d3cfa --- /dev/null +++ b/packages/capacitor/ios/capacitor-cordova-ios-plugins/CordovaPluginsResources.podspec @@ -0,0 +1,10 @@ +Pod::Spec.new do |s| + s.name = 'CordovaPluginsResources' + s.version = '0.0.105' + s.summary = 'Resources for Cordova plugins' + s.license = 'MIT' + s.homepage = 'https://capacitorjs.com/' + s.authors = { 'Ionic Team' => 'hi@ionicframework.com' } + s.source = { :git => 'https://github.com/ionic-team/capacitor.git', :tag => s.version.to_s } + s.resources = ['resources/*'] +end diff --git a/packages/capacitor/ios/capacitor-cordova-ios-plugins/resources/.gitkeep b/packages/capacitor/ios/capacitor-cordova-ios-plugins/resources/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/capacitor/ios/capacitor-cordova-ios-plugins/resources/.gitkeep @@ -0,0 +1 @@ + diff --git a/packages/capacitor/ios/capacitor-cordova-ios-plugins/sources/.gitkeep b/packages/capacitor/ios/capacitor-cordova-ios-plugins/sources/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/capacitor/ios/capacitor-cordova-ios-plugins/sources/.gitkeep @@ -0,0 +1 @@ + diff --git a/packages/capacitor/package.json b/packages/capacitor/package.json index c6549fc26..5ad82d84c 100644 --- a/packages/capacitor/package.json +++ b/packages/capacitor/package.json @@ -80,7 +80,9 @@ "rimraf": "^6.1.0", "rollup": "^4.53.2", "rollup-plugin-dts": "catalog:", - "swiftlint": "^2.0.0" + "swiftlint": "^2.0.0", + "vitest": "^4.1.1", + "@vitest/browser-preview": "^4.1.1" }, "peerDependencies": { "@capacitor/core": ">=8.0.0", diff --git a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts index 08d47fe3c..2a257659e 100644 --- a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts +++ b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts @@ -259,7 +259,8 @@ class CapacitorConnectionPool extends BaseObserver implements platform, values: param }) - })) + })), + false ); return { diff --git a/packages/capacitor/tests/basic.test.ts b/packages/capacitor/tests/basic.test.ts new file mode 100644 index 000000000..cc0105995 --- /dev/null +++ b/packages/capacitor/tests/basic.test.ts @@ -0,0 +1,291 @@ +import { column, LockContext, Schema, Table } from '@powersync/web'; +import { describe, expect, it, onTestFinished } from 'vitest'; +import { PowerSyncDatabase } from '../src/PowerSyncDatabase.js'; +import { CapacitorSQLiteAdapter } from '../src/adapter/CapacitorSQLiteAdapter.js'; + +const AppSchema = new Schema({ + users: new Table({ + name: column.text, + age: column.integer, + networth: column.real + }), + t1: new Table({ + a: column.integer, + b: column.integer, + c: column.text + }) +}); + +let userCounter = 0; +function generateUserInfo() { + userCounter += 1; + return { + id: `test-user-${userCounter}`, + name: `Test User ${userCounter}`, + age: 20 + userCounter, + networth: 1000.5 + userCounter + }; +} + +function createTestUser(context: Pick) { + const { name, age, networth } = generateUserInfo(); + return context.execute('INSERT INTO users (id, name, age, networth) VALUES(uuid(), ?, ?, ?)', [ + name, + age, + networth + ]); +} + +describe('Basic tests', () => { + function openDatabase(dbFilename: string) { + const database = new PowerSyncDatabase({ + database: { + dbFilename + }, + schema: AppSchema + }); + onTestFinished(async () => { + await database.disconnectAndClear().catch(() => {}); + await database.close().catch(() => {}); + }); + return database; + } + + /** + * We test in either ios/android - so we should use CapacitorSQLiteAdapter by default. + */ + it('should use native driver', async () => { + const database = openDatabase('native-driver'); + + expect(database.database).toBeInstanceOf(CapacitorSQLiteAdapter); + }); + + it('should insert', async () => { + const database = openDatabase('insert'); + + const res = await createTestUser(database); + + expect(res.rows?._array).toEqual([]); + expect(res.rows?.length).toBe(0); + expect(res.rows?.item).toBeTypeOf('function'); + }); + + it('should query without params', async () => { + const database = openDatabase('query-without-params'); + const { name, age, networth } = generateUserInfo(); + await database.execute('INSERT INTO users (id, name, age, networth) VALUES(uuid(), ?, ?, ?)', [ + name, + age, + networth + ]); + + const res = await database.execute('SELECT name, age, networth FROM users'); + + expect(res.rows?.length).toBe(1); + expect(res.rows?._array).toEqual([{ name, age, networth }]); + }); + + it('should query with params', async () => { + const database = openDatabase('query-with-params'); + const { id, name, age, networth } = generateUserInfo(); + await database.execute('INSERT INTO users (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth + ]); + + const res = await database.execute('SELECT name, age, networth FROM users WHERE id = ?', [id]); + + expect(res.rows?._array).toEqual([{ name, age, networth }]); + }); + + it('should reject failed inserts', async () => { + const database = openDatabase('failed-insert'); + const { name, networth } = generateUserInfo(); + + await expect( + database.execute('INSERT INTO usersfail (id, name, age, networth) VALUES(uuid(), ?, ?, ?)', [ + name, + name, + networth + ]) + ).rejects.toThrow(/no such table/i); + }); + + it('should auto commit transactions', async () => { + const database = openDatabase('transaction-auto-commit'); + const { name, age, networth } = generateUserInfo(); + + await database.writeTransaction(async (tx) => { + const res = await tx.execute('INSERT INTO "users" (id, name, age, networth) VALUES(uuid(), ?, ?, ?)', [ + name, + age, + networth + ]); + + expect(res.rows?._array).toEqual([]); + expect(res.rows?.length).toBe(0); + expect(res.rows?.item).toBeTypeOf('function'); + }); + + const res = await database.execute('SELECT name, age, networth FROM users'); + expect(res.rows?._array).toEqual([{ name, age, networth }]); + }); + + it('should auto rollback transactions on error', async () => { + const database = openDatabase('transaction-auto-rollback'); + const { name, age, networth } = generateUserInfo(); + + await expect( + database.writeTransaction(async (tx) => { + await tx.execute('INSERT INTO "users" (id, name, age, networth) VALUES(uuid(), ?, ?, ?)', [ + name, + age, + networth + ]); + throw new Error('rollback sentinel'); + }) + ).rejects.toThrow('rollback sentinel'); + + const res = await database.execute('SELECT * FROM users'); + expect(res.rows?._array).toEqual([]); + }); + + it('should manually commit transactions', async () => { + const database = openDatabase('transaction-manual-commit'); + const { name, age, networth } = generateUserInfo(); + + await database.writeTransaction(async (tx) => { + await tx.execute('INSERT INTO "users" (id, name, age, networth) VALUES(uuid(), ?, ?, ?)', [ + name, + age, + networth + ]); + await tx.commit(); + }); + + const res = await database.execute('SELECT name, age, networth FROM users'); + expect(res.rows?._array).toEqual([{ name, age, networth }]); + }); + + it('should manually rollback transactions', async () => { + const database = openDatabase('transaction-manual-rollback'); + const { name, age, networth } = generateUserInfo(); + + await database.writeTransaction(async (tx) => { + await tx.execute('INSERT INTO "users" (id, name, age, networth) VALUES(uuid(), ?, ?, ?)', [ + name, + age, + networth + ]); + await tx.rollback(); + }); + + const res = await database.execute('SELECT * FROM users'); + expect(res.rows?._array).toEqual([]); + }); + + it('should reject writeLock callback errors', async () => { + const database = openDatabase('write-lock-callback-error'); + + await expect( + database.writeLock(async () => { + throw new Error('Error from callback'); + }) + ).rejects.toThrow('Error from callback'); + }); + + it('should reject transaction callback errors', async () => { + const database = openDatabase('transaction-callback-error'); + + await expect( + database.writeTransaction(async () => { + throw new Error('Error from callback'); + }) + ).rejects.toThrow('Error from callback'); + }); + + it('should reject invalid transaction queries', async () => { + const database = openDatabase('transaction-invalid-query'); + + await expect( + database.writeTransaction(async (tx) => { + await tx.execute('SELECT * FROM [tableThatDoesNotExist];'); + }) + ).rejects.toThrow(/no such table: tableThatDoesNotExist/i); + }); + + it('should batch execute', async () => { + const database = openDatabase('batch-execute'); + const { id: id1, name: name1, age: age1, networth: networth1 } = generateUserInfo(); + const { id: id2, name: name2, age: age2, networth: networth2 } = generateUserInfo(); + + await database.executeBatch('INSERT INTO "users" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + [id1, name1, age1, networth1], + [id2, name2, age2, networth2] + ]); + + const expected = [ + { id: id1, name: name1, age: age1, networth: networth1 }, + { id: id2, name: name2, age: age2, networth: networth2 } + ].sort((a, b) => a.name.localeCompare(b.name)); + + const res = await database.execute('SELECT id, name, age, networth FROM users ORDER BY name'); + expect(res.rows?._array).toEqual(expected); + }); + + it('should keep read locks read only', async () => { + const database = openDatabase('read-lock-read-only'); + const { id, name, age, networth } = generateUserInfo(); + + await expect( + database.readLock(async (context) => { + await context.execute('INSERT INTO "users" (id, name, age, networth) VALUES(?, ?, ?, ?)', [ + id, + name, + age, + networth + ]); + }) + ).rejects.toThrow(/readonly|read.?only|not an error/i); + }); + + /** + * The native driver should allow for concurrent writes/reads. + */ + it('should read while a writeLock is held', async () => { + const database = openDatabase('read-while-write-lock'); + + let releaseWriteLock!: () => void; + const writeLockRelease = new Promise((resolve) => { + releaseWriteLock = resolve; + }); + + let writeLockStarted!: () => void; + const writeLockStart = new Promise((resolve) => { + writeLockStarted = resolve; + }); + + const writeLock = database.writeLock(async () => { + writeLockStarted(); + await writeLockRelease; + }); + + await writeLockStart; + + const result = await Promise.race([ + database.readLock(async (context) => { + const row = await context.get<{ value: number }>('SELECT 42 AS value'); + releaseWriteLock(); + return row.value; + }), + new Promise((_, reject) => { + setTimeout(() => reject(new Error('Timed out waiting for readLock while writeLock was held')), 2_000); + }) + ]); + + expect(result).toBe(42); + await writeLock; + }); +}); diff --git a/packages/capacitor/tests/setup/capacitor.ts b/packages/capacitor/tests/setup/capacitor.ts new file mode 100644 index 000000000..f1f898936 --- /dev/null +++ b/packages/capacitor/tests/setup/capacitor.ts @@ -0,0 +1,19 @@ +async function installCapacitorInTestFrame() { + if (window.top == null || window.top === window) { + return; + } + + const root = window.top as Window & typeof globalThis; + const current = window as Window & typeof globalThis; + + // await waitForCapacitorBridge(root); + + // Capacitor injects the native bridge into the top-level webview, but Vitest + // runs browser tests inside an iframe. Forward the bridge globals before + // @capacitor/core initializes in the test frame so native plugins resolve. + (current as any).Capacitor = (root as any).Capacitor; + (current as any).webkit = (root as any).webkit; + (current as any).androidBridge = (root as any).androidBridge; +} + +await installCapacitorInTestFrame(); diff --git a/packages/capacitor/vitest.config.ts b/packages/capacitor/vitest.config.ts new file mode 100644 index 000000000..616163d3e --- /dev/null +++ b/packages/capacitor/vitest.config.ts @@ -0,0 +1,148 @@ +import { spawn, spawnSync } from 'node:child_process'; + +import { preview } from '@vitest/browser-preview'; +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; +import { BrowserProvider } from 'vitest/node'; + +// We can't define serverFactory ourselves because vitest doesn't export the building blocks, +// but it boils down to [this](https://github.com/vitest-dev/vitest/blob/faace1fbe09133fa3641164c1d58538b316a38ee/packages/browser/src/node/index.ts#L25) +// for all browser providers, so we can just take that from any existing provider. +const serverFactory = preview().serverFactory; + +/** + * The same app which will load the Vitest url + */ +const EXAMPLE_APP_DIR = path.resolve(import.meta.dirname, 'example-app'); + +function requireTestEnvironmentVariable(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing ${name}. See packages/capacitor/DEVELOP.md for native integration test setup.`); + } + return value; +} + +/** + * We use environment variables to trigger tests. See DEVELOP.md for setup instructions. + */ +const environment = { + /** + * ios | android. + */ + platform: requireTestEnvironmentVariable('TEST_PLATFORM'), + /** + * The device target id to run on. + */ + target: requireTestEnvironmentVariable('TEST_TARGET'), + /** + * Hostname the app should use to reach the Vitest server. Android emulators use + * 10.0.2.2 to connect to the host machine instead of the emulator loopback. + */ + serverHost: process.env.TEST_SERVER_HOST ?? (process.env.TEST_PLATFORM == 'android' ? '10.0.2.2' : undefined) +}; + +function serverUrlForPlatform(url: string): string { + if (environment.platform != 'android') { + return url; + } + + /** + * For convenience, we replace localhost with 10.0.2.2 on Android, + * this avoids having to use `adb reverse`. + */ + + const resolvedUrl = new URL(url); + resolvedUrl.hostname = environment.serverHost ?? resolvedUrl.hostname; + return resolvedUrl.toString(); +} + +class CapacitorBrowserProvider implements BrowserProvider { + get name(): string { + return 'capacitor'; + } + + get supportsParallelism(): boolean { + return false; + } + + getCommandsContext(_sessionId: string): Record { + return {}; + } + + async openPage(_sessionId: string, url: string, _options?: { parallel: boolean }) { + const serverUrl = serverUrlForPlatform(url); + console.log(`Opening Capacitor app with Vitest URL: ${serverUrl}`); + + // Ensure the target app spawning webviews is up-to-date with the current Vitest server URL. + const buildResult = spawnSync('npx', ['cap', 'sync', environment.platform], { + stdio: 'inherit', + cwd: EXAMPLE_APP_DIR, + env: { + ...process.env, + CAPACITOR_VITEST_SERVER_URL: serverUrl + } + }); + if (buildResult.status !== 0) { + throw new Error( + `cap sync failed with ${buildResult.signal ? `signal ${buildResult.signal}` : `exit code ${buildResult.status}`}` + ); + } + console.log(`Launching ${environment.platform} Capacitor app on ${environment.target}`); + + const app = spawn('npx', ['cap', 'run', environment.platform, '--target', environment.target, '--no-sync'], { + cwd: EXAMPLE_APP_DIR, + env: { + ...process.env, + // The Capacitor App will load this URL on boot. Android emulators use 10.0.2.2 to reach the host. + CAPACITOR_VITEST_SERVER_URL: serverUrl + }, + stdio: 'inherit' + }); + + // The process to run the Capacitor app will end once the app starts, + // we don't keep track of it, but we do fail if the command failed. + await new Promise((resolve, reject) => { + app.once('exit', (code, signal) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`cap run failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`)); + } + }); + app.once('error', reject); + }); + + console.log('Remote browser should be open'); + } + + async close() {} +} + +export default defineConfig({ + server: { + // Android emulators connect to the host through 10.0.2.2, so Vitest must listen beyond loopback. + host: '0.0.0.0' + }, + test: { + include: ['tests/**/*.test.ts'], + setupFiles: ['tests/setup/capacitor.ts'], + isolate: false, + browser: { + enabled: true, + provider: { + name: 'capacitor-app', + options: {}, + providerFactory() { + return new CapacitorBrowserProvider(); + }, + serverFactory + }, + instances: [ + { + browser: 'chrome' + } + ] + } + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae3fc5dd4..f3ae842e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -367,6 +367,9 @@ importers: '@ionic/swiftlint-config': specifier: ^2.0.0 version: 2.0.0 + '@vitest/browser-preview': + specifier: ^4.1.1 + version: 4.1.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.1.1) eslint: specifier: ^8.57.1 version: 8.57.1 @@ -388,6 +391,9 @@ importers: swiftlint: specifier: ^2.0.0 version: 2.0.0(typescript@5.9.3) + vitest: + specifier: ^4.1.1 + version: 4.1.1(@types/node@24.10.13)(@vitest/browser-preview@4.1.1)(jsdom@24.1.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) packages/common: dependencies: