Skip to content

Instantly share code, notes, and snippets.

@exonomyapp
Last active February 13, 2025 17:17
Show Gist options
  • Save exonomyapp/2113818fb519ea7db71c3295747d7778 to your computer and use it in GitHub Desktop.
Save exonomyapp/2113818fb519ea7db71c3295747d7778 to your computer and use it in GitHub Desktop.
Single Codebase for Tauri and Capacitor

Here’s how you could structure a single codebase to support both Tauri (desktop) and Capacitor (mobile), leveraging shared logic while accommodating platform-specific needs:


Project Structure

your-app/
├── src/                   # Shared frontend code (Nuxt + Ionic + TypeScript)
│   ├── app.vue            # Root component
│   ├── composables/       # Shared business logic (e.g., P2P networking)
│   ├── components/        # Shared UI components (Ionic components here)
│   └── assets/            # Shared images/styles
│
├── tauri/                 # Tauri-specific configuration (desktop)
│   ├── src/               # Rust backend logic (P2P encryption, file I/O)
│   └── tauri.conf.json    # Tauri config (bundle IDs, desktop settings)
│
├── capacitor/             # Capacitor-specific config (mobile)
│   ├── android/           # Android platform code
│   ├── ios/               # iOS platform code
│   └── capacitor.config.ts # Mobile-specific settings
│
├── .nuxt/                 # Nuxt auto-generated files
├── nuxt.config.ts         # Nuxt config (SSR disabled, shared plugins)
├── package.json           # Shared dependencies + scripts
└── tsconfig.json          # Shared TypeScript config

Code Sharing Strategies

1. UI Components (Ionic)

  • Use Ionic’s web components for cross-platform UI (e.g., <ion-button>, <ion-list>). They render consistently in both Tauri’s WebView and Capacitor’s native WebView.
  • Conditionally apply platform-specific styles using Ionic’s platform utilities:
    // Shared component
    import { isPlatform } from '@ionic/vue';
    
    if (isPlatform('desktop')) {
      // Tauri-specific UI tweaks (e.g., hover states)
    } else if (isPlatform('mobile')) {
      // Mobile-specific gestures (e.g., swipe-to-refresh)
    }

2. Platform-Specific Logic

  • Shared Interface: Define a TypeScript interface for platform-agnostic APIs (e.g., file system access, P2P networking):
    // src/composables/p2p.ts
    interface P2PInterface {
      sendData(data: string): Promise<void>;
      receiveData(): Promise<string>;
    }
  • Tauri Implementation (Rust backend):
    // tauri/src/p2p.rs
    #[tauri::command]
    fn send_data(data: String) {
      // Rust logic for desktop P2P (e.g., libp2p)
    }
  • Capacitor Implementation (TypeScript + Plugins):
    // capacitor/src/p2p.ts
    import { Network } from '@capacitor/network';
    
    export const sendData = async (data: string) => {
      await Network.connect(); // Use Capacitor’s network plugin
      // Mobile P2P logic (e.g., WebRTC)
    };

3. Platform Detection

  • Use compile-time flags to exclude platform-specific code during builds:
    // package.json
    {
      "scripts": {
        "build:desktop": "NUXT_PLATFORM=tauri nuxt build",
        "build:mobile": "NUXT_PLATFORM=capacitor nuxt build"
      }
    }
  • In code:
    if (process.env.NUXT_PLATFORM === 'tauri') {
      // Load Tauri-specific modules
    } else {
      // Load Capacitor-specific modules
    }

Build Workflow

  1. Shared Frontend:

    • Nuxt compiles the app into static files (dist/).
    • Ionic components are tree-shaken for desktop/mobile targets.
  2. Desktop (Tauri):

    npm run build:desktop
    cd tauri && tauri build
    • Bundles the Nuxt dist/ with Tauri’s Rust backend.
  3. Mobile (Capacitor):

    npm run build:mobile
    npx cap sync android && npx cap sync ios
    • Syncs the Nuxt dist/ with Capacitor’s native projects.

Key Challenges & Solutions

Challenge Solution
Native Feature Parity Use dependency injection to swap Tauri (Rust) and Capacitor (TypeScript) implementations.
UI Consistency Test Ionic components in both WebViews; override styles with CSS variables.
Build Complexity Use a monorepo tool (e.g., Turborepo) to manage parallel builds.
Plugin Mismatches Wrap Tauri/Capacitor plugins in a shared interface (e.g., StorageService).

Pros & Cons

Pros:

  • Maximized code reuse: 80-90% shared code (UI, business logic).
  • Desktop-first + mobile fallback: Optimize for Tauri’s strengths while using Capacitor for mature mobile support.
  • Flexible native access: Combine Rust (desktop) and Capacitor plugins (mobile).

Cons:

  • Double maintenance: Two native layers (Rust for Tauri, TypeScript for Capacitor).
  • Complex debugging: Requires testing on 5 platforms (3 desktop + 2 mobile).
  • Build tooling overhead: Separate configs for Tauri/Capacitor.

When to Choose This Approach

  • You need desktop-grade performance (Tauri’s Rust backend) but native mobile UX (Capacitor).
  • Your team can manage Rust (Tauri) and TypeScript (Capacitor) codebases.
  • The app’s core logic (e.g., P2P networking) can be abstracted from platform specifics.

For a streamlined workflow, consider using Turborepo or Nx to orchestrate shared code and platform-specific builds.


Notes about Turborepo

Using Turborepo in this scenario would streamline your monorepo setup, allowing you to manage shared code, platform-specific builds (Tauri/Capacitor), and cross-platform dependencies efficiently. Here’s how you’d structure and configure it:

1. Monorepo Structure with Turborepo

your-app/
├── apps/
│   ├── tauri-desktop/      # Tauri app (macOS, Windows, Linux)
│   │   ├── src-tauri/      # Rust backend
│   │   ├── public/         # Web assets (shared via symlink)
│   │   └── package.json    # Tauri-specific deps
│   │
│   └── capacitor-mobile/   # Capacitor app (iOS/Android)
│       ├── android/        # Android platform code
│       ├── ios/            # iOS platform code
│       └── package.json    # Capacitor-specific deps
│
├── packages/
│   ├── ui/                 # Shared Ionic UI components (Vue)
│   │   ├── components/     # <ion-button>, <ion-list>, etc.
│   │   └── package.json    # Shared UI dependencies
│   │
│   ├── core/               # Shared business logic (TypeScript)
│   │   ├── p2p/            # P2P networking abstractions
│   │   ├── storage/        # Data storage interface
│   │   └── package.json    
│   │
│   └── config/             # Shared configs (Nuxt, TS, ESLint)
│       └── nuxt.config.ts  # Base Nuxt config
│
├── turbo.json              # Turborepo task pipeline
├── package.json            # Root workspace deps
└── pnpm-workspace.yaml     # Workspace config (if using pnpm)

2. Key Turborepo Configurations

turbo.json – Task Orchestration

Define pipelines to handle shared and platform-specific tasks:

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"], // Build dependencies first
      "outputs": ["dist/**"],  // Cache outputs
      "cache": true
    },
    "dev": {
      "cache": false, // Disable cache for dev servers
      "persistent": true
    },
    "lint": {
      "outputs": []
    }
  }
}

pnpm-workspace.yaml – Workspace Setup

packages:
  - 'apps/*'
  - 'packages/*'

3. Shared Code Strategy

UI Components (packages/ui)

  • Share Ionic components across apps using TypeScript path aliases:
    // apps/tauri-desktop/nuxt.config.ts
    import { defineNuxtConfig } from '@nuxt/bridge';
    import { join } from 'path';
    
    export default defineNuxtConfig({
      alias: {
        '@ui': join(__dirname, '../../packages/ui'),
      },
    });
  • Use in components:
    <script setup>
    import { IonButton } from '@ui/components';
    </script>

Business Logic (packages/core)

  • Abstract platform-specific logic with dependency injection:
    // packages/core/p2p/interface.ts
    export interface P2PProvider {
      send(data: string): Promise<void>;
    }
    
    // apps/tauri-desktop/p2p.ts (Tauri impl)
    import { invoke } from '@tauri-apps/api';
    export const TauriP2P: P2PProvider = {
      send: (data) => invoke('send_data', { data }),
    };
    
    // apps/capacitor-mobile/p2p.ts (Capacitor impl)
    import { Network } from '@capacitor/network';
    export const CapacitorP2P: P2PProvider = {
      send: async (data) => {
        await Network.connect();
        // WebRTC logic...
      },
    };

4. Platform-Specific Builds

Tauri Desktop Workflow

# Build shared packages first
turbo run build --filter=core... --filter=ui...

# Build Tauri app
cd apps/tauri-desktop
pnpm tauri build

Capacitor Mobile Workflow

# Build shared packages
turbo run build --filter=core... --filter=ui...

# Sync Capacitor with shared web assets
cd apps/capacitor-mobile
pnpm cap sync

5. Turborepo Caching for Efficiency

Turborepo automatically caches:

  • Build outputs (e.g., dist/, .nuxt/)
  • Dependencies (node_modules)
  • Task results (e.g., TypeScript type-checking)

Example: After modifying a shared UI component, only dependent apps rebuild:

turbo run build --filter=ui...  # Rebuilds UI package and apps depending on it

6. Handling Platform-Specific Dependencies

Use conditional imports in shared code:

// packages/core/storage.ts
import { isTauri } from '../utils/platform';

export const saveData = async (data: string) => {
  if (isTauri()) {
    const { writeFile } = await import('@tauri-apps/api/fs');
    await writeFile({ path: 'data.txt', contents: data });
  } else {
    const { Filesystem } = await import('@capacitor/filesystem');
    await Filesystem.writeFile({ path: 'data.txt', data });
  }
};

7. Development Workflow

Run parallel dev servers for all platforms:

# Start Tauri desktop dev
turbo run dev --filter=tauri-desktop

# Start Capacitor mobile dev
turbo run dev --filter=capacitor-mobile

Key Benefits of Turborepo Here

  1. Shared Code Isolation:
    • UI, core logic, and configs are versioned as separate packages.
  2. Parallelized Builds:
    • Build Tauri and Capacitor apps simultaneously.
  3. Deterministic Caching:
    • Skip redundant builds (e.g., unchanged UI components).
  4. Cross-Platform Testing:
    turbo run test # Runs tests in all workspaces

Potential Pitfalls

  • Dependency Conflicts: Ensure Tauri (Rust) and Capacitor (Java/Swift) toolchains don’t clash.
  • Platform Detection Overhead: Abstracting platform-specific code adds complexity.
  • Cache Invalidation: Turborepo may occasionally require turbo run build --force for major dependency updates.

Final Setup

For a working example, clone the Turborepo + Tauri + Capacitor starter (hypothetical template – adapt as needed). This structure lets you maintain a single codebase while leveraging Turborepo’s optimizations for multi-platform development.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment