Browse Source

fix: SQLite plugin initialization in Electron main process

- Changed from direct plugin usage to SQLiteConnection pattern
- Matches how platform services use the SQLite plugin
- Removed handle() method dependency
- Added proper method routing in IPC handler

The app now launches without initialization errors. Next steps:
- Test actual SQLite operations (createConnection, query, etc.)
- Verify database creation and access
- Add error handling for database operations
pull/134/head
Matthew Raymer 1 week ago
parent
commit
70c0edbed0
  1. 11
      capacitor.config.json
  2. 5
      package.json
  3. 18
      scripts/build-electron.js
  4. 104
      src/electron/main.ts
  5. 1
      src/main.common.ts
  6. 7
      src/main.electron.ts
  7. 204
      vite.config.electron.mts

11
capacitor.config.json

@ -1,10 +1,11 @@
{ {
"appId": "app.timesafari", "appId": "com.timesafari.app",
"appName": "TimeSafari", "appName": "TimeSafari",
"webDir": "dist", "webDir": "dist",
"bundledWebRuntime": false, "bundledWebRuntime": false,
"server": { "server": {
"cleartext": true "cleartext": true,
"androidScheme": "https"
}, },
"plugins": { "plugins": {
"App": { "App": {
@ -29,6 +30,12 @@
"biometricAuth": true, "biometricAuth": true,
"biometricTitle": "Biometric login for TimeSafari" "biometricTitle": "Biometric login for TimeSafari"
} }
},
"CapacitorSQLite": {
"electronIsEncryption": false,
"electronMacLocation": "~/Library/Application Support/TimeSafari",
"electronWindowsLocation": "C:\\ProgramData\\TimeSafari",
"electronLinuxLocation": "~/.local/share/TimeSafari"
} }
}, },
"ios": { "ios": {

5
package.json

@ -23,7 +23,7 @@
"clean:electron": "rimraf dist-electron", "clean:electron": "rimraf dist-electron",
"build:pywebview": "vite build --config vite.config.pywebview.mts", "build:pywebview": "vite build --config vite.config.pywebview.mts",
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts", "build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
"build:web:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts --mode electron", "build:web:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts && VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts --mode electron",
"build:electron": "npm run clean:electron && npm run build:web:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.js", "build:electron": "npm run clean:electron && npm run build:web:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts", "build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
"electron:dev": "npm run build && electron .", "electron:dev": "npm run build && electron .",
@ -181,7 +181,8 @@
}, },
"files": [ "files": [
"dist-electron/**/*", "dist-electron/**/*",
"dist/**/*" "dist/**/*",
"capacitor.config.json"
], ],
"extraResources": [ "extraResources": [
{ {

18
scripts/build-electron.js

@ -1,6 +1,7 @@
const fs = require("fs"); const fs = require("fs");
const fse = require("fs-extra"); const fse = require("fs-extra");
const path = require("path"); const path = require("path");
const { execSync } = require('child_process');
console.log("Starting Electron build finalization..."); console.log("Starting Electron build finalization...");
@ -64,4 +65,21 @@ if (fs.existsSync(preloadSrc)) {
console.error("Preload script not found at:", preloadSrc); console.error("Preload script not found at:", preloadSrc);
} }
// Copy capacitor.config.json to dist-electron
try {
console.log("Copying capacitor.config.json to dist-electron...");
const configPath = path.join(process.cwd(), 'capacitor.config.json');
const targetPath = path.join(process.cwd(), 'dist-electron', 'capacitor.config.json');
if (!fs.existsSync(configPath)) {
throw new Error('capacitor.config.json not found in project root');
}
fs.copyFileSync(configPath, targetPath);
console.log("Successfully copied capacitor.config.json");
} catch (error) {
console.error("Failed to copy capacitor.config.json:", error);
throw error;
}
console.log("Electron index.html copied and patched for Electron context."); console.log("Electron index.html copied and patched for Electron context.");

104
src/electron/main.ts

@ -1,7 +1,8 @@
import { app, BrowserWindow, ipcMain } from "electron"; import { app, BrowserWindow, ipcMain } from "electron";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
import { CapacitorSQLiteElectron } from "@capacitor-community/sqlite/electron/dist/plugin"; import { Capacitor } from "@capacitor/core";
import { SQLiteConnection } from "@capacitor-community/sqlite";
// Simple logger implementation // Simple logger implementation
const logger = { const logger = {
@ -17,12 +18,94 @@ const logger = {
debug: (...args: unknown[]) => console.debug("[Main]", ...args), debug: (...args: unknown[]) => console.debug("[Main]", ...args),
}; };
logger.info("Starting main process initialization...");
// Initialize Capacitor for Electron in main process
try {
logger.info("About to initialize Capacitor...");
logger.info("Capacitor before init:", {
hasPlatform: 'platform' in Capacitor,
hasIsNativePlatform: 'isNativePlatform' in Capacitor,
platformType: typeof Capacitor.platform,
isNativePlatformType: typeof Capacitor.isNativePlatform
});
// Try direct assignment first
try {
(Capacitor as any).platform = 'electron';
(Capacitor as any).isNativePlatform = true;
logger.info("Direct assignment successful");
} catch (e) {
logger.warn("Direct assignment failed, trying defineProperty:", e);
Object.defineProperty(Capacitor, "isNativePlatform", {
get: () => true,
configurable: true,
});
Object.defineProperty(Capacitor, "platform", {
get: () => 'electron',
configurable: true,
});
}
logger.info("Capacitor after init:", {
platform: Capacitor.platform,
isNativePlatform: Capacitor.isNativePlatform,
platformType: typeof Capacitor.platform,
isNativePlatformType: typeof Capacitor.isNativePlatform
});
} catch (error) {
logger.error("Failed to initialize Capacitor:", error);
throw error;
}
// Initialize SQLite plugin for Electron // Initialize SQLite plugin for Electron
let sqlitePlugin: CapacitorSQLiteElectron | null = null; let sqliteConnection: SQLiteConnection | null = null;
try { try {
logger.info("Initializing SQLite plugin..."); logger.info("About to initialize SQLite plugin...");
sqlitePlugin = new CapacitorSQLiteElectron(); // Use require to ensure we get the CommonJS module
const sqliteModule = require('@capacitor-community/sqlite/electron/dist/plugin');
logger.info("SQLite module loaded:", Object.keys(sqliteModule));
// Debug the module contents
logger.info("SQLite module contents:", {
default: typeof sqliteModule.default,
defaultKeys: sqliteModule.default ? Object.keys(sqliteModule.default) : [],
CapacitorSQLite: typeof sqliteModule.CapacitorSQLite,
CapacitorSQLiteKeys: sqliteModule.CapacitorSQLite ? Object.keys(sqliteModule.CapacitorSQLite) : [],
moduleType: typeof sqliteModule,
moduleKeys: Object.keys(sqliteModule),
hasHandle: sqliteModule.default?.handle ? 'yes' : 'no',
handleType: typeof sqliteModule.default?.handle,
defaultProto: Object.getPrototypeOf(sqliteModule.default),
defaultProtoKeys: Object.getPrototypeOf(sqliteModule.default) ? Object.keys(Object.getPrototypeOf(sqliteModule.default)) : []
});
// Get the plugin constructor
const SQLitePlugin = sqliteModule.CapacitorSQLite;
logger.info("Got SQLite plugin constructor:", {
type: typeof SQLitePlugin,
name: SQLitePlugin?.name,
prototype: SQLitePlugin ? Object.getPrototypeOf(SQLitePlugin) : null,
prototypeKeys: SQLitePlugin ? Object.keys(Object.getPrototypeOf(SQLitePlugin)) : []
});
if (typeof SQLitePlugin !== 'function') {
throw new Error(`SQLite plugin is not a constructor, got ${typeof SQLitePlugin}`);
}
// Create a connection using the plugin
logger.info("Creating SQLite connection...");
sqliteConnection = new SQLiteConnection(SQLitePlugin);
logger.info("SQLite connection created:", {
type: typeof sqliteConnection,
keys: Object.keys(sqliteConnection),
prototype: Object.getPrototypeOf(sqliteConnection),
prototypeKeys: Object.getPrototypeOf(sqliteConnection) ? Object.keys(Object.getPrototypeOf(sqliteConnection)) : []
});
logger.info("SQLite plugin initialized successfully"); logger.info("SQLite plugin initialized successfully");
} catch (error) { } catch (error) {
logger.error("Failed to initialize SQLite plugin:", error); logger.error("Failed to initialize SQLite plugin:", error);
@ -31,15 +114,22 @@ try {
// Set up IPC handler for SQLite // Set up IPC handler for SQLite
ipcMain.handle("capacitor-sqlite", async (_event, ...args) => { ipcMain.handle("capacitor-sqlite", async (_event, ...args) => {
if (!sqlitePlugin) { if (!sqliteConnection) {
const error = new Error("SQLite plugin not initialized"); const error = new Error("SQLite connection not initialized");
logger.error(error); logger.error(error);
throw error; throw error;
} }
try { try {
logger.debug("Handling SQLite request:", args); logger.debug("Handling SQLite request:", args);
const result = await sqlitePlugin.handle(_event, ...args); // The first argument should be the method name
const [method, ...restArgs] = args;
if (typeof method !== 'string' || !(method in sqliteConnection)) {
throw new Error(`Invalid SQLite method: ${method}`);
}
// Call the method on the connection
const result = await (sqliteConnection as any)[method](...restArgs);
logger.debug("SQLite request completed successfully"); logger.debug("SQLite request completed successfully");
return result; return result;
} catch (error) { } catch (error) {

1
src/main.common.ts

@ -2,6 +2,7 @@ import { createPinia } from "pinia";
import { App as VueApp, ComponentPublicInstance, createApp } from "vue"; import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
// Use the browser version of axios for web builds
import axios from "axios"; import axios from "axios";
import VueAxios from "vue-axios"; import VueAxios from "vue-axios";
import Notifications from "notiwind"; import Notifications from "notiwind";

7
src/main.electron.ts

@ -1,14 +1,7 @@
import { initializeApp } from "./main.common"; import { initializeApp } from "./main.common";
import { logger } from "./utils/logger"; import { logger } from "./utils/logger";
import { Capacitor } from "@capacitor/core";
import { CapacitorSQLite } from "@capacitor-community/sqlite"; import { CapacitorSQLite } from "@capacitor-community/sqlite";
// Initialize Capacitor for Electron
Object.defineProperty(Capacitor, "isNativePlatform", {
get: () => true,
configurable: true,
});
// Initialize SQLite plugin for Electron // Initialize SQLite plugin for Electron
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// Register the plugin globally // Register the plugin globally

204
vite.config.electron.mts

@ -1,120 +1,102 @@
import { defineConfig, mergeConfig } from "vite"; import { defineConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts";
import path from 'path'; import path from 'path';
export default defineConfig(async ({ mode }) => { export default defineConfig({
const baseConfig = await createBuildConfig('electron'); build: {
const isWebBuild = mode === 'electron'; outDir: 'dist-electron',
rollupOptions: {
return mergeConfig(baseConfig, { input: {
build: { main: path.resolve(__dirname, 'src/electron/main.ts'),
outDir: isWebBuild ? 'dist' : 'dist-electron', preload: path.resolve(__dirname, 'src/electron/preload.js'),
rollupOptions: isWebBuild ? {
// Web app build configuration
input: path.resolve(__dirname, 'index.html'),
output: {
format: 'esm',
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]'
}
} : {
// Main process build configuration
input: {
main: path.resolve(__dirname, 'src/electron/main.ts'),
preload: path.resolve(__dirname, 'src/electron/preload.js'),
},
external: [
'electron',
'@capacitor-community/sqlite',
'@capacitor-community/sqlite/electron',
'better-sqlite3-multiple-ciphers'
],
output: {
format: 'cjs',
entryFileNames: '[name].js',
assetFileNames: 'assets/[name].[ext]',
},
}, },
target: isWebBuild ? 'esnext' : 'node18', external: [
minify: !isWebBuild, // Node.js built-ins
sourcemap: true, 'stream',
}, 'path',
resolve: { 'fs',
alias: { 'crypto',
'@': path.resolve(__dirname, 'src'), 'buffer',
'util',
'events',
'url',
'assert',
'os',
'net',
'http',
'https',
'zlib',
'child_process',
// Electron and Capacitor
'electron',
'@capacitor/core',
'@capacitor-community/sqlite',
'@capacitor-community/sqlite/electron',
'@capacitor-community/sqlite/electron/dist/plugin',
'better-sqlite3-multiple-ciphers',
// HTTP clients
'axios',
'axios/dist/axios',
'axios/dist/node/axios.cjs'
],
output: {
format: 'cjs',
entryFileNames: '[name].js',
assetFileNames: 'assets/[name].[ext]',
}, },
}, },
optimizeDeps: { target: 'node18',
include: ['@/utils/logger'] minify: false,
sourcemap: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
// Use Node.js version of axios in electron
'axios': 'axios/dist/node/axios.cjs'
}, },
plugins: [ },
{ optimizeDeps: {
name: 'typescript-transform', exclude: [
transform(code: string, id: string) { 'stream',
if (id.endsWith('main.ts')) { 'path',
// Replace the logger import with inline logger 'fs',
return code.replace( 'crypto',
/import\s*{\s*logger\s*}\s*from\s*['"]@\/utils\/logger['"];?/, 'buffer',
`const logger = { 'util',
log: (...args) => console.log(...args), 'events',
error: (...args) => console.error(...args), 'url',
info: (...args) => console.info(...args), 'assert',
warn: (...args) => console.warn(...args), 'os',
debug: (...args) => console.debug(...args), 'net',
};` 'http',
); 'https',
} 'zlib',
return code; 'child_process',
} 'axios',
}, 'axios/dist/axios',
{ 'axios/dist/node/axios.cjs'
name: 'remove-sw-imports', ]
transform(code: string, id: string) { },
// Remove service worker imports and registrations plugins: [
if (id.includes('registerServiceWorker') || {
id.includes('register-service-worker') || name: 'typescript-transform',
id.includes('sw_scripts') || transform(code: string, id: string) {
id.includes('PushNotificationPermission') || if (id.endsWith('main.ts')) {
code.includes('navigator.serviceWorker')) { return code.replace(
return { /import\s*{\s*logger\s*}\s*from\s*['"]@\/utils\/logger['"];?/,
code: code `const logger = {
.replace(/import.*registerServiceWorker.*$/mg, '') log: (...args) => console.log(...args),
.replace(/import.*register-service-worker.*$/mg, '') error: (...args) => console.error(...args),
.replace(/navigator\.serviceWorker/g, 'undefined') info: (...args) => console.info(...args),
.replace(/if\s*\([^)]*serviceWorker[^)]*\)\s*{[^}]*}/g, '') warn: (...args) => console.warn(...args),
.replace(/import.*workbox.*$/mg, '') debug: (...args) => console.debug(...args),
.replace(/importScripts\([^)]*\)/g, '') };`
}; );
}
return code;
}
},
{
name: 'remove-sw-files',
enforce: 'pre',
resolveId(id: string) {
// Prevent service worker files from being included
if (id.includes('sw.js') ||
id.includes('workbox') ||
id.includes('registerSW.js') ||
id.includes('manifest.webmanifest')) {
return '\0empty';
}
return null;
},
load(id: string) {
if (id === '\0empty') {
return 'export default {}';
}
return null;
} }
return code;
} }
], }
ssr: { ],
noExternal: ['@/utils/logger'] base: './',
}, publicDir: 'public',
base: './',
publicDir: 'public',
});
}); });
Loading…
Cancel
Save