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