From 3ec2364394e71a7b072fbadfca405853ea34b76e Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 3 Jun 2025 04:06:24 +0000 Subject: [PATCH] refactor: update electron preload script and type definitions This commit updates the Electron preload script and type definitions to improve SQLite integration and IPC communication. The changes include: - Enhanced preload script (electron/src/preload.ts): * Added detailed logging for SQLite operations and IPC communication * Implemented retry logic for SQLite operations (3 attempts, 1s delay) * Added proper type definitions for SQLite connection options * Defined strict channel validation for IPC communication * Improved error handling and logging throughout - Updated type definitions (src/types/electron.d.ts): * Aligned ElectronAPI interface with actual implementation * Added proper typing for all SQLite operations * Added environment variables (platform, isDev) * Structured IPC renderer interface with proper method signatures Current Status: - Preload script initializes successfully - SQLite availability check works (returns true) - SQLite ready signal is properly received - Database operations are failing with two types of errors: 1. "CapacitorSQLite not available" during initialization 2. "Cannot read properties of undefined" for SQLite methods Next Steps: - Verify context bridge exposure in renderer process - Check main process SQLite handlers - Debug database initialization - Address Content Security Policy warning Affected Files: - Modified: electron/src/preload.ts - Modified: src/types/electron.d.ts Note: This is a transitional commit. While the preload script and type definitions are now properly structured, database operations are not yet functional. Further debugging and fixes are required to resolve the SQLite integration issues. --- electron/src/preload.ts | 119 +++++++++++++++++++++++++------------ scripts/build-electron.cjs | 17 +++++- src/types/electron.d.ts | 22 +++++-- tsconfig.electron.json | 7 ++- vite.config.electron.mts | 1 + 5 files changed, 119 insertions(+), 47 deletions(-) diff --git a/electron/src/preload.ts b/electron/src/preload.ts index f63d7abf..fb058374 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -7,13 +7,21 @@ import { contextBridge, ipcRenderer } from 'electron'; -// Simple logger for preload script +// Enhanced logger for preload script const logger = { log: (...args: unknown[]) => console.log('[Preload]', ...args), error: (...args: unknown[]) => console.error('[Preload]', ...args), info: (...args: unknown[]) => console.info('[Preload]', ...args), warn: (...args: unknown[]) => console.warn('[Preload]', ...args), debug: (...args: unknown[]) => console.debug('[Preload]', ...args), + sqlite: { + log: (operation: string, ...args: unknown[]) => + console.log('[Preload][SQLite]', operation, ...args), + error: (operation: string, error: unknown) => + console.error('[Preload][SQLite]', operation, 'failed:', error), + debug: (operation: string, ...args: unknown[]) => + console.debug('[Preload][SQLite]', operation, ...args) + } }; // Types for SQLite connection options @@ -30,38 +38,72 @@ interface SQLiteConnectionOptions { // Define valid channels for security const VALID_CHANNELS = { - send: ['toMain', 'sqlite-status'], - receive: ['fromMain', 'sqlite-ready', 'database-status'], - invoke: ['sqlite-is-available', 'sqlite-echo', 'sqlite-create-connection', 'sqlite-execute', 'sqlite-query', 'sqlite-run', 'sqlite-close-connection', 'get-path', 'get-base-path'] + send: ['toMain', 'sqlite-status'] as const, + receive: ['fromMain', 'sqlite-ready', 'database-status'] as const, + invoke: [ + 'sqlite-is-available', + 'sqlite-echo', + 'sqlite-create-connection', + 'sqlite-execute', + 'sqlite-query', + 'sqlite-run', + 'sqlite-close-connection', + 'get-path', + 'get-base-path' + ] as const }; +type ValidSendChannel = typeof VALID_CHANNELS.send[number]; +type ValidReceiveChannel = typeof VALID_CHANNELS.receive[number]; +type ValidInvokeChannel = typeof VALID_CHANNELS.invoke[number]; + // Create a secure IPC bridge const createSecureIPCBridge = () => { return { send: (channel: string, data: unknown) => { - if (VALID_CHANNELS.send.includes(channel)) { + if (VALID_CHANNELS.send.includes(channel as ValidSendChannel)) { + logger.debug('IPC Send:', channel, data); ipcRenderer.send(channel, data); } else { logger.warn(`[Preload] Attempted to send on invalid channel: ${channel}`); } }, + receive: (channel: string, func: (...args: unknown[]) => void) => { - if (VALID_CHANNELS.receive.includes(channel)) { - ipcRenderer.on(channel, (_event, ...args) => func(...args)); + if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) { + logger.debug('IPC Receive:', channel); + ipcRenderer.on(channel, (_event, ...args) => { + logger.debug('IPC Received:', channel, args); + func(...args); + }); } else { logger.warn(`[Preload] Attempted to receive on invalid channel: ${channel}`); } }, + once: (channel: string, func: (...args: unknown[]) => void) => { - if (VALID_CHANNELS.receive.includes(channel)) { - ipcRenderer.once(channel, (_event, ...args) => func(...args)); + if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) { + logger.debug('IPC Once:', channel); + ipcRenderer.once(channel, (_event, ...args) => { + logger.debug('IPC Received Once:', channel, args); + func(...args); + }); } else { logger.warn(`[Preload] Attempted to receive once on invalid channel: ${channel}`); } }, + invoke: async (channel: string, ...args: unknown[]) => { - if (VALID_CHANNELS.invoke.includes(channel)) { - return await ipcRenderer.invoke(channel, ...args); + if (VALID_CHANNELS.invoke.includes(channel as ValidInvokeChannel)) { + logger.debug('IPC Invoke:', channel, args); + try { + const result = await ipcRenderer.invoke(channel, ...args); + logger.debug('IPC Invoke Result:', channel, result); + return result; + } catch (error) { + logger.error('IPC Invoke Error:', channel, error); + throw error; + } } else { logger.warn(`[Preload] Attempted to invoke on invalid channel: ${channel}`); throw new Error(`Invalid channel: ${channel}`); @@ -75,57 +117,60 @@ const createSQLiteProxy = () => { const MAX_RETRIES = 3; const RETRY_DELAY = 1000; - const withRetry = async (operation: (...args: unknown[]) => Promise, ...args: unknown[]) => { - let lastError; + const withRetry = async (operation: string, ...args: unknown[]): Promise => { + let lastError: Error | undefined; + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { - return await operation(...args); + logger.sqlite.debug(operation, 'attempt', attempt, args); + const result = await ipcRenderer.invoke(`sqlite-${operation}`, ...args); + logger.sqlite.log(operation, 'success', result); + return result as T; } catch (error) { - lastError = error; + lastError = error instanceof Error ? error : new Error(String(error)); + logger.sqlite.error(operation, error); if (attempt < MAX_RETRIES) { - logger.warn(`[Preload] SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error); + logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); } } } - throw new Error(`SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`); - }; - - const wrapOperation = (method: string) => { - return async (...args: unknown[]) => { - try { - const handlerName = `sqlite-${method}`; - return await withRetry(ipcRenderer.invoke, handlerName, ...args); - } catch (error) { - logger.error(`[Preload] SQLite ${method} failed:`, error); - throw error; - } - }; + + throw new Error(`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`); }; return { - echo: wrapOperation('echo'), - createConnection: wrapOperation('create-connection'), - closeConnection: wrapOperation('close-connection'), - execute: wrapOperation('execute'), - query: wrapOperation('query'), - run: wrapOperation('run'), - isAvailable: wrapOperation('is-available'), + isAvailable: () => withRetry('is-available'), + echo: (value: string) => withRetry('echo', { value }), + createConnection: (options: SQLiteConnectionOptions) => withRetry('create-connection', options), + closeConnection: (options: { database: string }) => withRetry('close-connection', options), + query: (options: { statement: string; values?: unknown[] }) => withRetry('query', options), + run: (options: { statement: string; values?: unknown[] }) => withRetry('run', options), + execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => withRetry('execute', options), getPlatform: () => Promise.resolve('electron') }; }; try { // Expose the secure IPC bridge and SQLite proxy - contextBridge.exposeInMainWorld('electron', { + const electronAPI = { ipcRenderer: createSecureIPCBridge(), sqlite: createSQLiteProxy(), env: { platform: 'electron', isDev: process.env.NODE_ENV === 'development' } + }; + + // Log the exposed API for debugging + logger.debug('Exposing Electron API:', { + hasIpcRenderer: !!electronAPI.ipcRenderer, + hasSqlite: !!electronAPI.sqlite, + sqliteMethods: Object.keys(electronAPI.sqlite), + env: electronAPI.env }); + contextBridge.exposeInMainWorld('electron', electronAPI); logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully'); } catch (error) { logger.error('[Preload] Failed to initialize IPC bridge:', error); diff --git a/scripts/build-electron.cjs b/scripts/build-electron.cjs index 0a601184..6b5066a0 100644 --- a/scripts/build-electron.cjs +++ b/scripts/build-electron.cjs @@ -49,7 +49,7 @@ indexContent = indexContent.replace( fs.writeFileSync(finalIndexPath, indexContent); // Copy preload script to resources -const preloadSrc = path.join(electronDistPath, "preload.js"); +const preloadSrc = path.join(electronDistPath, "preload.mjs"); const preloadDest = path.join(electronDistPath, "resources", "preload.js"); // Ensure resources directory exists @@ -59,10 +59,21 @@ if (!fs.existsSync(resourcesDir)) { } if (fs.existsSync(preloadSrc)) { - fs.copyFileSync(preloadSrc, preloadDest); - console.log("Preload script copied to resources directory"); + // Read the preload script + let preloadContent = fs.readFileSync(preloadSrc, 'utf-8'); + + // Convert ESM to CommonJS if needed + preloadContent = preloadContent + .replace(/import\s*{\s*([^}]+)\s*}\s*from\s*['"]electron['"];?/g, 'const { $1 } = require("electron");') + .replace(/export\s*{([^}]+)};?/g, '') + .replace(/export\s+default\s+([^;]+);?/g, 'module.exports = $1;'); + + // Write the modified preload script + fs.writeFileSync(preloadDest, preloadContent); + console.log("Preload script copied and converted to resources directory"); } else { console.error("Preload script not found at:", preloadSrc); + process.exit(1); } // Copy capacitor.config.json to dist-electron diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 3096a809..5892bfaa 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -1,11 +1,23 @@ interface ElectronAPI { sqlite: { isAvailable: () => Promise; - execute: (method: string, ...args: unknown[]) => Promise; + echo: (value: string) => Promise<{ value: string }>; + createConnection: (options: { + database: string; + version?: number; + readOnly?: boolean; + readonly?: boolean; + encryption?: string; + mode?: string; + useNative?: boolean; + [key: string]: unknown; + }) => Promise; + closeConnection: (options: { database: string }) => Promise; + query: (options: { statement: string; values?: unknown[] }) => Promise; + run: (options: { statement: string; values?: unknown[] }) => Promise; + execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise; + getPlatform: () => Promise; }; - getPath: (pathType: string) => Promise; - send: (channel: string, data: unknown) => void; - receive: (channel: string, func: (...args: unknown[]) => void) => void; ipcRenderer: { on: (channel: string, func: (...args: unknown[]) => void) => void; once: (channel: string, func: (...args: unknown[]) => void) => void; @@ -14,7 +26,9 @@ interface ElectronAPI { }; env: { platform: string; + isDev: boolean; }; + getPath: (pathType: string) => Promise; getBasePath: () => Promise; } diff --git a/tsconfig.electron.json b/tsconfig.electron.json index c667c18e..a7e70a1f 100644 --- a/tsconfig.electron.json +++ b/tsconfig.electron.json @@ -5,7 +5,7 @@ "moduleResolution": "bundler", "target": "ES2020", "outDir": "dist-electron", - "rootDir": "src", + "rootDir": ".", "sourceMap": true, "esModuleInterop": true, "allowJs": true, @@ -13,7 +13,7 @@ "isolatedModules": true, "noEmit": true, "allowImportingTsExtensions": true, - "types": ["vite/client"], + "types": ["vite/client", "electron"], "paths": { "@/*": ["./src/*"] }, @@ -23,6 +23,7 @@ "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", - "src/**/*.vue" + "src/**/*.vue", + "electron/src/**/*.ts" ] } \ No newline at end of file diff --git a/vite.config.electron.mts b/vite.config.electron.mts index 83739535..8aa80255 100644 --- a/vite.config.electron.mts +++ b/vite.config.electron.mts @@ -7,6 +7,7 @@ export default defineConfig({ rollupOptions: { input: { main: path.resolve(__dirname, 'src/electron/main.ts'), + preload: path.resolve(__dirname, 'electron/src/preload.ts') }, external: [ // Node.js built-ins