Browse Source
The preload script (src/electron/preload.js) was removed as part of a refactor to separate web and electron builds. This script was previously responsible for: - Secure IPC communication between electron main and renderer processes - SQLite database access bridge for the renderer process - Context isolation and API exposure for electron-specific features Current state: - Web app builds successfully without preload script - Electron builds fail due to missing preload script - SQLite initialization works in main process but renderer can't access it - Database operations fail with "Cannot read properties of undefined" This commit is a breaking change for electron builds. The preload script will need to be recreated to restore electron database functionality. Affected files: - Deleted: src/electron/preload.js - Modified: src/main.electron.ts (removed DatabaseManager import) - Modified: src/utils/logger.ts (simplified logging implementation) - Modified: src/types/electron.d.ts (updated ElectronAPI interface) - Modified: src/types/global.d.ts (updated window.electron type definition) Next steps: - Recreate preload script with proper SQLite bridge - Update electron build configuration - Restore database access in renderer processsql-absurd-sql-further
5 changed files with 24 additions and 1089 deletions
@ -1,174 +0,0 @@ |
|||
const { app, BrowserWindow } = require("electron"); |
|||
const path = require("path"); |
|||
const fs = require("fs"); |
|||
const logger = require("../utils/logger"); |
|||
|
|||
// Check if running in dev mode
|
|||
const isDev = process.argv.includes("--inspect"); |
|||
|
|||
function createWindow() { |
|||
// Add before createWindow function
|
|||
const preloadPath = path.join(__dirname, "preload.js"); |
|||
logger.log("Checking preload path:", preloadPath); |
|||
logger.log("Preload exists:", fs.existsSync(preloadPath)); |
|||
|
|||
// Create the browser window.
|
|||
const mainWindow = new BrowserWindow({ |
|||
width: 1200, |
|||
height: 800, |
|||
webPreferences: { |
|||
nodeIntegration: false, |
|||
contextIsolation: true, |
|||
webSecurity: true, |
|||
allowRunningInsecureContent: false, |
|||
preload: path.join(__dirname, "preload.js"), |
|||
}, |
|||
}); |
|||
|
|||
// Always open DevTools for now
|
|||
mainWindow.webContents.openDevTools(); |
|||
|
|||
// Intercept requests to fix asset paths
|
|||
mainWindow.webContents.session.webRequest.onBeforeRequest( |
|||
{ |
|||
urls: [ |
|||
"file://*/*/assets/*", |
|||
"file://*/assets/*", |
|||
"file:///assets/*", // Catch absolute paths
|
|||
"<all_urls>", // Catch all URLs as a fallback
|
|||
], |
|||
}, |
|||
(details, callback) => { |
|||
let url = details.url; |
|||
|
|||
// Handle paths that don't start with file://
|
|||
if (!url.startsWith("file://") && url.includes("/assets/")) { |
|||
url = `file://${path.join(__dirname, "www", url)}`; |
|||
} |
|||
|
|||
// Handle absolute paths starting with /assets/
|
|||
if (url.includes("/assets/") && !url.includes("/www/assets/")) { |
|||
const baseDir = url.includes("dist-electron") |
|||
? url.substring( |
|||
0, |
|||
url.indexOf("/dist-electron") + "/dist-electron".length, |
|||
) |
|||
: `file://${__dirname}`; |
|||
const assetPath = url.split("/assets/")[1]; |
|||
const newUrl = `${baseDir}/www/assets/${assetPath}`; |
|||
callback({ redirectURL: newUrl }); |
|||
return; |
|||
} |
|||
|
|||
callback({}); // No redirect for other URLs
|
|||
}, |
|||
); |
|||
|
|||
if (isDev) { |
|||
// Debug info
|
|||
logger.log("Debug Info:"); |
|||
logger.log("Running in dev mode:", isDev); |
|||
logger.log("App is packaged:", app.isPackaged); |
|||
logger.log("Process resource path:", process.resourcesPath); |
|||
logger.log("App path:", app.getAppPath()); |
|||
logger.log("__dirname:", __dirname); |
|||
logger.log("process.cwd():", process.cwd()); |
|||
} |
|||
|
|||
const indexPath = path.join(__dirname, "www", "index.html"); |
|||
|
|||
if (isDev) { |
|||
logger.log("Loading index from:", indexPath); |
|||
logger.log("www path:", path.join(__dirname, "www")); |
|||
logger.log("www assets path:", path.join(__dirname, "www", "assets")); |
|||
} |
|||
|
|||
if (!fs.existsSync(indexPath)) { |
|||
logger.error(`Index file not found at: ${indexPath}`); |
|||
throw new Error("Index file not found"); |
|||
} |
|||
|
|||
// Add CSP headers to allow API connections
|
|||
mainWindow.webContents.session.webRequest.onHeadersReceived( |
|||
(details, callback) => { |
|||
callback({ |
|||
responseHeaders: { |
|||
...details.responseHeaders, |
|||
"Content-Security-Policy": [ |
|||
"default-src 'self';" + |
|||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" + |
|||
"img-src 'self' data: https: blob:;" + |
|||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" + |
|||
"style-src 'self' 'unsafe-inline';" + |
|||
"font-src 'self' data:;", |
|||
], |
|||
}, |
|||
}); |
|||
}, |
|||
); |
|||
|
|||
// Load the index.html
|
|||
mainWindow |
|||
.loadFile(indexPath) |
|||
.then(() => { |
|||
logger.log("Successfully loaded index.html"); |
|||
if (isDev) { |
|||
mainWindow.webContents.openDevTools(); |
|||
logger.log("DevTools opened - running in dev mode"); |
|||
} |
|||
}) |
|||
.catch((err) => { |
|||
logger.error("Failed to load index.html:", err); |
|||
logger.error("Attempted path:", indexPath); |
|||
}); |
|||
|
|||
// Listen for console messages from the renderer
|
|||
mainWindow.webContents.on("console-message", (_event, level, message) => { |
|||
logger.log("Renderer Console:", message); |
|||
}); |
|||
|
|||
// Add right after creating the BrowserWindow
|
|||
mainWindow.webContents.on( |
|||
"did-fail-load", |
|||
(event, errorCode, errorDescription) => { |
|||
logger.error("Page failed to load:", errorCode, errorDescription); |
|||
}, |
|||
); |
|||
|
|||
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => { |
|||
logger.error("Preload script error:", preloadPath, error); |
|||
}); |
|||
|
|||
mainWindow.webContents.on( |
|||
"console-message", |
|||
(event, level, message, line, sourceId) => { |
|||
logger.log("Renderer Console:", line, sourceId, message); |
|||
}, |
|||
); |
|||
|
|||
// Enable remote debugging when in dev mode
|
|||
if (isDev) { |
|||
mainWindow.webContents.openDevTools(); |
|||
} |
|||
} |
|||
|
|||
// Handle app ready
|
|||
app.whenReady().then(createWindow); |
|||
|
|||
// Handle all windows closed
|
|||
app.on("window-all-closed", () => { |
|||
if (process.platform !== "darwin") { |
|||
app.quit(); |
|||
} |
|||
}); |
|||
|
|||
app.on("activate", () => { |
|||
if (BrowserWindow.getAllWindows().length === 0) { |
|||
createWindow(); |
|||
} |
|||
}); |
|||
|
|||
// Handle any errors
|
|||
process.on("uncaughtException", (error) => { |
|||
logger.error("Uncaught Exception:", error); |
|||
}); |
@ -1,712 +0,0 @@ |
|||
import { app, BrowserWindow, ipcMain } from "electron"; |
|||
import { Capacitor } from "@capacitor/core"; |
|||
import fs from "fs"; |
|||
import path from "path"; |
|||
import { fileURLToPath } from "url"; |
|||
|
|||
// Get __dirname equivalent in ES modules
|
|||
const __filename = fileURLToPath(import.meta.url); |
|||
const __dirname = path.dirname(__filename); |
|||
|
|||
// Set global variables that the plugin needs
|
|||
global.__dirname = __dirname; |
|||
global.__filename = __filename; |
|||
|
|||
// Now import the plugin after setting up globals
|
|||
import { CapacitorSQLite } from "@capacitor-community/sqlite/electron/dist/plugin.js"; |
|||
|
|||
// Simple logger implementation
|
|||
const logger = { |
|||
// eslint-disable-next-line no-console
|
|||
log: (...args: unknown[]) => console.log("[Main]", ...args), |
|||
// eslint-disable-next-line no-console
|
|||
error: (...args: unknown[]) => console.error("[Main]", ...args), |
|||
// eslint-disable-next-line no-console
|
|||
info: (...args: unknown[]) => console.info("[Main]", ...args), |
|||
// eslint-disable-next-line no-console
|
|||
warn: (...args: unknown[]) => console.warn("[Main]", ...args), |
|||
// eslint-disable-next-line no-console
|
|||
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 unknown as { platform: string }).platform = "electron"; |
|||
(Capacitor as unknown as { isNativePlatform: boolean }).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; |
|||
} |
|||
|
|||
// Database path resolution utilities
|
|||
const getAppDataPath = async (): Promise<string> => { |
|||
try { |
|||
// Read config file directly
|
|||
const configPath = path.join(__dirname, "..", "capacitor.config.json"); |
|||
const configContent = await fs.promises.readFile(configPath, "utf-8"); |
|||
const config = JSON.parse(configContent); |
|||
const linuxPath = config?.plugins?.CapacitorSQLite?.electronLinuxLocation; |
|||
|
|||
if (linuxPath) { |
|||
// Expand ~ to home directory
|
|||
const expandedPath = linuxPath.replace(/^~/, process.env.HOME || ""); |
|||
logger.info("[Electron Main] Using configured database path:", expandedPath); |
|||
return expandedPath; |
|||
} |
|||
|
|||
// Fallback to app.getPath if config path is not available
|
|||
const userDataPath = app.getPath("userData"); |
|||
logger.info("[Electron Main] Using fallback user data path:", userDataPath); |
|||
return userDataPath; |
|||
} catch (error) { |
|||
logger.error("[Electron Main] Error getting app data path:", error); |
|||
// Fallback to app.getPath if anything fails
|
|||
const userDataPath = app.getPath("userData"); |
|||
logger.info( |
|||
"[Electron Main] Using fallback user data path after error:", |
|||
userDataPath, |
|||
); |
|||
return userDataPath; |
|||
} |
|||
}; |
|||
|
|||
const validateAndNormalizePath = async (filePath: string): Promise<string> => { |
|||
// Resolve any relative paths
|
|||
const resolvedPath = path.resolve(filePath); |
|||
|
|||
// Ensure it's an absolute path
|
|||
if (!path.isAbsolute(resolvedPath)) { |
|||
throw new Error(`Database path must be absolute: ${resolvedPath}`); |
|||
} |
|||
|
|||
// Ensure it's within the app data directory
|
|||
const appDataPath = await getAppDataPath(); |
|||
if (!resolvedPath.startsWith(appDataPath)) { |
|||
throw new Error( |
|||
`Database path must be within app data directory: ${resolvedPath}`, |
|||
); |
|||
} |
|||
|
|||
// Normalize the path
|
|||
const normalizedPath = path.normalize(resolvedPath); |
|||
|
|||
logger.info("[Electron Main] Validated database path:", { |
|||
original: filePath, |
|||
resolved: resolvedPath, |
|||
normalized: normalizedPath, |
|||
appDataPath, |
|||
isAbsolute: path.isAbsolute(normalizedPath), |
|||
isWithinAppData: normalizedPath.startsWith(appDataPath), |
|||
}); |
|||
|
|||
return normalizedPath; |
|||
}; |
|||
|
|||
const ensureDirectoryExists = async (dirPath: string): Promise<void> => { |
|||
try { |
|||
// Normalize the path first
|
|||
const normalizedPath = path.normalize(dirPath); |
|||
|
|||
// Check if directory exists
|
|||
if (!fs.existsSync(normalizedPath)) { |
|||
logger.info("[Electron Main] Creating database directory:", normalizedPath); |
|||
await fs.promises.mkdir(normalizedPath, { recursive: true }); |
|||
} |
|||
|
|||
// Verify directory permissions
|
|||
try { |
|||
await fs.promises.access( |
|||
normalizedPath, |
|||
fs.constants.R_OK | fs.constants.W_OK, |
|||
); |
|||
logger.info( |
|||
"[Electron Main] Database directory permissions verified:", |
|||
normalizedPath, |
|||
); |
|||
} catch (error) { |
|||
logger.error("[Electron Main] Database directory permission error:", error); |
|||
throw new Error(`Database directory not accessible: ${normalizedPath}`); |
|||
} |
|||
|
|||
// Test write permissions
|
|||
const testFile = path.join(normalizedPath, ".write-test"); |
|||
try { |
|||
await fs.promises.writeFile(testFile, "test"); |
|||
await fs.promises.unlink(testFile); |
|||
logger.info( |
|||
"[Electron Main] Database directory write test passed:", |
|||
normalizedPath, |
|||
); |
|||
} catch (error) { |
|||
logger.error("[Electron Main] Database directory write test failed:", error); |
|||
throw new Error(`Database directory not writable: ${normalizedPath}`); |
|||
} |
|||
} catch (error) { |
|||
logger.error( |
|||
"[Electron Main] Failed to ensure database directory exists:", |
|||
error, |
|||
); |
|||
throw error; |
|||
} |
|||
}; |
|||
|
|||
// Initialize database paths
|
|||
let dbPath: string | undefined; |
|||
let dbDir: string | undefined; |
|||
let dbPathInitialized = false; |
|||
let dbPathInitializationPromise: Promise<void> | null = null; |
|||
|
|||
const initializeDatabasePaths = async (): Promise<void> => { |
|||
// Prevent multiple simultaneous initializations
|
|||
if (dbPathInitializationPromise) { |
|||
return dbPathInitializationPromise; |
|||
} |
|||
|
|||
if (dbPathInitialized) { |
|||
return; |
|||
} |
|||
|
|||
dbPathInitializationPromise = (async () => { |
|||
try { |
|||
// Get the base directory from config
|
|||
dbDir = await getAppDataPath(); |
|||
logger.info("[Electron Main] Database directory:", dbDir); |
|||
|
|||
// Ensure the directory exists and is writable
|
|||
await ensureDirectoryExists(dbDir); |
|||
|
|||
// Construct the database path
|
|||
dbPath = await validateAndNormalizePath( |
|||
path.join(dbDir, "timesafari.db"), |
|||
); |
|||
logger.info("[Electron Main] Database path initialized:", dbPath); |
|||
|
|||
// Verify the database file if it exists
|
|||
if (fs.existsSync(dbPath)) { |
|||
try { |
|||
await fs.promises.access( |
|||
dbPath, |
|||
fs.constants.R_OK | fs.constants.W_OK, |
|||
); |
|||
logger.info( |
|||
"[Electron Main] Existing database file permissions verified:", |
|||
dbPath, |
|||
); |
|||
} catch (error) { |
|||
logger.error("[Electron Main] Database file permission error:", error); |
|||
throw new Error(`Database file not accessible: ${dbPath}`); |
|||
} |
|||
} |
|||
|
|||
dbPathInitialized = true; |
|||
} catch (error) { |
|||
logger.error("[Electron Main] Failed to initialize database paths:", error); |
|||
throw error; |
|||
} finally { |
|||
dbPathInitializationPromise = null; |
|||
} |
|||
})(); |
|||
|
|||
return dbPathInitializationPromise; |
|||
}; |
|||
|
|||
// Initialize SQLite plugin
|
|||
let sqlitePlugin: any = null; |
|||
let sqliteInitialized = false; |
|||
let sqliteInitializationPromise: Promise<void> | null = null; |
|||
|
|||
async function initializeSQLite() { |
|||
// Prevent multiple simultaneous initializations
|
|||
if (sqliteInitializationPromise) { |
|||
return sqliteInitializationPromise; |
|||
} |
|||
|
|||
if (sqliteInitialized) { |
|||
return; |
|||
} |
|||
|
|||
sqliteInitializationPromise = (async () => { |
|||
try { |
|||
logger.info("[Electron Main] Initializing SQLite plugin..."); |
|||
sqlitePlugin = new CapacitorSQLite(); |
|||
|
|||
// Initialize database paths first
|
|||
await initializeDatabasePaths(); |
|||
|
|||
if (!dbPath) { |
|||
throw new Error("Database path not initialized"); |
|||
} |
|||
|
|||
// Test the plugin
|
|||
const echoResult = await sqlitePlugin.echo({ value: "test" }); |
|||
logger.info("[Electron Main] SQLite plugin echo test:", echoResult); |
|||
|
|||
// Initialize database connection using validated dbPath
|
|||
const connectionOptions = { |
|||
database: dbPath, |
|||
version: 1, |
|||
readOnly: false, |
|||
encryption: "no-encryption", |
|||
useNative: true, |
|||
mode: "rwc", // Force read-write-create mode
|
|||
}; |
|||
|
|||
logger.info( |
|||
"[Electron Main] Creating initial connection with options:", |
|||
connectionOptions, |
|||
); |
|||
|
|||
// Log the actual path being used
|
|||
logger.info("[Electron Main] Using database path:", dbPath); |
|||
logger.info("[Electron Main] Path exists:", fs.existsSync(dbPath)); |
|||
logger.info("[Electron Main] Path is absolute:", path.isAbsolute(dbPath)); |
|||
|
|||
const db = await sqlitePlugin.createConnection(connectionOptions); |
|||
|
|||
if (!db || typeof db !== "object") { |
|||
throw new Error( |
|||
`Failed to create database connection - invalid response. Path used: ${dbPath}`, |
|||
); |
|||
} |
|||
|
|||
// Wait a moment for the connection to be fully established
|
|||
await new Promise((resolve) => setTimeout(resolve, 100)); |
|||
|
|||
// Verify the connection is working
|
|||
try { |
|||
const result = await db.query("PRAGMA journal_mode;"); |
|||
logger.info("[Electron Main] Database connection verified:", result); |
|||
} catch (error) { |
|||
logger.error( |
|||
"[Electron Main] Database connection verification failed:", |
|||
error, |
|||
); |
|||
throw error; |
|||
} |
|||
|
|||
sqliteInitialized = true; |
|||
logger.info("[Electron Main] SQLite plugin initialized successfully"); |
|||
} catch (error) { |
|||
logger.error("[Electron Main] Failed to initialize SQLite plugin:", error); |
|||
throw error; |
|||
} finally { |
|||
sqliteInitializationPromise = null; |
|||
} |
|||
})(); |
|||
|
|||
return sqliteInitializationPromise; |
|||
} |
|||
|
|||
// Initialize app when ready
|
|||
app.whenReady().then(async () => { |
|||
logger.info("App is ready, starting initialization..."); |
|||
|
|||
// Create window first
|
|||
const mainWindow = createWindow(); |
|||
|
|||
// Wait for window to be ready
|
|||
await new Promise<void>((resolve) => { |
|||
mainWindow.once('ready-to-show', () => { |
|||
logger.info("[Electron Main] Window ready to show"); |
|||
mainWindow.show(); |
|||
resolve(); |
|||
}); |
|||
}); |
|||
|
|||
// Initialize database after window is ready
|
|||
try { |
|||
await initializeSQLite(); |
|||
logger.info("[Electron Main] SQLite plugin initialized successfully"); |
|||
|
|||
// Now send the ready signal since window is ready
|
|||
if (!mainWindow.isDestroyed()) { |
|||
mainWindow.webContents.send('sqlite-ready'); |
|||
logger.info("[Electron Main] Sent SQLite ready signal to renderer"); |
|||
} else { |
|||
logger.error("[Electron Main] Window was destroyed before sending SQLite ready signal"); |
|||
} |
|||
} catch (error) { |
|||
logger.error( |
|||
"[Electron Main] Database initialization failed:", |
|||
error, |
|||
); |
|||
// Notify renderer about database status
|
|||
if (!mainWindow.isDestroyed()) { |
|||
mainWindow.webContents.send("database-status", { |
|||
status: "error", |
|||
error: error instanceof Error ? error.message : 'Unknown error', |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// Handle window close
|
|||
mainWindow.on("closed", () => { |
|||
logger.info("[Electron Main] Main window closed"); |
|||
}); |
|||
|
|||
// Handle window close request
|
|||
mainWindow.on("close", (event) => { |
|||
logger.info("[Electron Main] Window close requested"); |
|||
// Prevent immediate close if we're in the middle of something
|
|||
if (mainWindow.webContents.isLoading()) { |
|||
event.preventDefault(); |
|||
logger.info("[Electron Main] Deferring window close due to loading state"); |
|||
mainWindow.webContents.once("did-finish-load", () => { |
|||
mainWindow.close(); |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
// Check if running in dev mode
|
|||
// const isDev = process.argv.includes("--inspect");
|
|||
|
|||
function createWindow(): BrowserWindow { |
|||
// Resolve preload path based on environment
|
|||
const preloadPath = app.isPackaged |
|||
? path.join(process.resourcesPath, "preload.js") |
|||
: path.join(__dirname, "preload.js"); |
|||
|
|||
logger.log("[Electron Main] Preload path:", preloadPath); |
|||
logger.log("[Electron Main] Preload exists:", fs.existsSync(preloadPath)); |
|||
|
|||
// Log environment and paths
|
|||
logger.log("[Electron Main] process.cwd():", process.cwd()); |
|||
logger.log("[Electron Main] __dirname:", __dirname); |
|||
logger.log("[Electron Main] app.getAppPath():", app.getAppPath()); |
|||
logger.log("[Electron Main] app.isPackaged:", app.isPackaged); |
|||
logger.log("[Electron Main] process.resourcesPath:", process.resourcesPath); |
|||
|
|||
// List files in __dirname and __dirname/www
|
|||
try { |
|||
logger.log("Files in __dirname:", fs.readdirSync(__dirname)); |
|||
const wwwDir = path.join(__dirname, "www"); |
|||
if (fs.existsSync(wwwDir)) { |
|||
logger.log("Files in www:", fs.readdirSync(wwwDir)); |
|||
} else { |
|||
logger.log("www directory does not exist in __dirname"); |
|||
} |
|||
} catch (e) { |
|||
logger.error("Error reading directories:", e); |
|||
} |
|||
|
|||
// Create the browser window with proper error handling
|
|||
const mainWindow = new BrowserWindow({ |
|||
width: 1200, |
|||
height: 800, |
|||
show: false, // Don't show until ready
|
|||
webPreferences: { |
|||
nodeIntegration: false, |
|||
contextIsolation: true, |
|||
sandbox: false, |
|||
preload: preloadPath, |
|||
webSecurity: true, |
|||
allowRunningInsecureContent: false, |
|||
}, |
|||
}); |
|||
|
|||
// Show window when ready
|
|||
mainWindow.once("ready-to-show", () => { |
|||
logger.info("[Electron Main] Window ready to show"); |
|||
mainWindow.show(); |
|||
}); |
|||
|
|||
// Handle window errors
|
|||
mainWindow.webContents.on("render-process-gone", (_event, details) => { |
|||
logger.error("[Electron Main] Render process gone:", details); |
|||
}); |
|||
|
|||
mainWindow.webContents.on( |
|||
"did-fail-load", |
|||
(_event, errorCode, errorDescription) => { |
|||
logger.error( |
|||
"[Electron Main] Page failed to load:", |
|||
errorCode, |
|||
errorDescription, |
|||
); |
|||
logger.error("[Electron Main] Failed URL:", mainWindow.webContents.getURL()); |
|||
}, |
|||
); |
|||
|
|||
// Load the index.html
|
|||
let indexPath: string; |
|||
let fileUrl: string; |
|||
|
|||
if (app.isPackaged) { |
|||
indexPath = path.join(process.resourcesPath, "www", "index.html"); |
|||
fileUrl = `file://${indexPath}`; |
|||
logger.info( |
|||
"[Electron Main] App is packaged. Using process.resourcesPath for index.html", |
|||
); |
|||
} else { |
|||
indexPath = path.resolve(__dirname, "www", "index.html"); |
|||
fileUrl = `file://${indexPath}`; |
|||
logger.info( |
|||
"[Electron Main] App is not packaged. Using __dirname for index.html", |
|||
); |
|||
} |
|||
|
|||
logger.info("[Electron Main] Resolved index.html path:", indexPath); |
|||
logger.info("[Electron Main] Using file URL:", fileUrl); |
|||
|
|||
// Load the index.html with retry logic
|
|||
const loadIndexHtml = async (retryCount = 0): Promise<void> => { |
|||
try { |
|||
if (mainWindow.isDestroyed()) { |
|||
logger.error( |
|||
"[Electron Main] Window was destroyed before loading index.html", |
|||
); |
|||
return; |
|||
} |
|||
|
|||
const exists = fs.existsSync(indexPath); |
|||
logger.info(`[Electron Main] fs.existsSync for index.html: ${exists}`); |
|||
|
|||
if (!exists) { |
|||
throw new Error(`index.html not found at path: ${indexPath}`); |
|||
} |
|||
|
|||
// Try to read the file to verify it's accessible
|
|||
const stats = fs.statSync(indexPath); |
|||
logger.info("[Electron Main] index.html stats:", { |
|||
size: stats.size, |
|||
mode: stats.mode, |
|||
uid: stats.uid, |
|||
gid: stats.gid, |
|||
}); |
|||
|
|||
// Try loadURL first
|
|||
try { |
|||
logger.info("[Electron Main] Attempting to load index.html via loadURL"); |
|||
await mainWindow.loadURL(fileUrl); |
|||
logger.info("[Electron Main] Successfully loaded index.html via loadURL"); |
|||
} catch (loadUrlError) { |
|||
logger.warn( |
|||
"[Electron Main] loadURL failed, trying loadFile:", |
|||
loadUrlError, |
|||
); |
|||
// Fallback to loadFile
|
|||
await mainWindow.loadFile(indexPath); |
|||
logger.info("[Electron Main] Successfully loaded index.html via loadFile"); |
|||
} |
|||
} catch (error: unknown) { |
|||
const errorMessage = |
|||
error instanceof Error ? error.message : "Unknown error occurred"; |
|||
logger.error("[Electron Main] Error loading index.html:", errorMessage); |
|||
|
|||
// Retry logic
|
|||
if (retryCount < 3 && !mainWindow.isDestroyed()) { |
|||
logger.info( |
|||
`[Electron Main] Retrying index.html load (attempt ${retryCount + 1})`, |
|||
); |
|||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
|||
return loadIndexHtml(retryCount + 1); |
|||
} |
|||
|
|||
// If we've exhausted retries, show error in window
|
|||
if (!mainWindow.isDestroyed()) { |
|||
const errorHtml = ` |
|||
<html> |
|||
<body style="font-family: sans-serif; padding: 20px;"> |
|||
<h1>Error Loading Application</h1> |
|||
<p>Failed to load the application after multiple attempts.</p> |
|||
<pre style="background: #f0f0f0; padding: 10px; border-radius: 4px;">${errorMessage}</pre> |
|||
</body> |
|||
</html> |
|||
`;
|
|||
await mainWindow.loadURL( |
|||
`data:text/html,${encodeURIComponent(errorHtml)}`, |
|||
); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
// Start loading the index.html
|
|||
loadIndexHtml().catch((error: unknown) => { |
|||
logger.error("[Electron Main] Fatal error loading index.html:", error); |
|||
}); |
|||
|
|||
// Only open DevTools if not in production
|
|||
if (!app.isPackaged) { |
|||
mainWindow.webContents.openDevTools({ mode: "detach" }); |
|||
} |
|||
|
|||
return mainWindow; |
|||
} |
|||
|
|||
// Handle app ready
|
|||
app.whenReady().then(createWindow); |
|||
|
|||
// Handle all windows closed
|
|||
app.on("window-all-closed", () => { |
|||
if (process.platform !== "darwin") { |
|||
app.quit(); |
|||
} |
|||
}); |
|||
|
|||
app.on("activate", () => { |
|||
if (BrowserWindow.getAllWindows().length === 0) { |
|||
createWindow(); |
|||
} |
|||
}); |
|||
|
|||
// Handle any errors
|
|||
process.on("uncaughtException", (error) => { |
|||
logger.error("Uncaught Exception:", error); |
|||
}); |
|||
|
|||
// Set up IPC handlers for SQLite operations
|
|||
ipcMain.handle("check-sqlite-availability", () => { |
|||
return sqlitePlugin !== null; |
|||
}); |
|||
|
|||
ipcMain.handle("sqlite-echo", async (_event, value) => { |
|||
try { |
|||
return await sqlitePlugin.echo({ value }); |
|||
} catch (error) { |
|||
logger.error( |
|||
"Error in sqlite-echo:", |
|||
error, |
|||
JSON.stringify(error), |
|||
(error as any)?.stack, |
|||
); |
|||
throw error; |
|||
} |
|||
}); |
|||
|
|||
ipcMain.handle("sqlite-create-connection", async (_event, options) => { |
|||
try { |
|||
// Ensure database is initialized
|
|||
await initializeSQLite(); |
|||
|
|||
if (!dbPath) { |
|||
throw new Error("Database path not initialized"); |
|||
} |
|||
|
|||
// Override any provided database path with our resolved path
|
|||
const connectionOptions = { |
|||
...options, |
|||
database: dbPath, |
|||
readOnly: false, |
|||
mode: "rwc", // Force read-write-create mode
|
|||
encryption: "no-encryption", |
|||
useNative: true, |
|||
}; |
|||
|
|||
logger.info( |
|||
"[Electron Main] Creating database connection with options:", |
|||
connectionOptions, |
|||
); |
|||
const result = await sqlitePlugin.createConnection(connectionOptions); |
|||
|
|||
if (!result || typeof result !== "object") { |
|||
throw new Error( |
|||
"Failed to create database connection - invalid response", |
|||
); |
|||
} |
|||
|
|||
// Wait a moment for the connection to be fully established
|
|||
await new Promise((resolve) => setTimeout(resolve, 100)); |
|||
|
|||
try { |
|||
// Verify connection is not read-only
|
|||
const testResult = await result.query({ |
|||
statement: "PRAGMA journal_mode;", |
|||
}); |
|||
if (testResult?.values?.[0]?.journal_mode === "off") { |
|||
logger.error( |
|||
"[Electron Main] Connection opened in read-only mode despite options", |
|||
); |
|||
throw new Error("Database connection opened in read-only mode"); |
|||
} |
|||
} catch (queryError) { |
|||
logger.error("[Electron Main] Error verifying connection:", queryError); |
|||
throw queryError; |
|||
} |
|||
|
|||
logger.info("[Electron Main] Database connection created successfully"); |
|||
return result; |
|||
} catch (error) { |
|||
logger.error("[Electron Main] Error in sqlite-create-connection:", error); |
|||
throw error; |
|||
} |
|||
}); |
|||
|
|||
ipcMain.handle("sqlite-execute", async (_event, options) => { |
|||
try { |
|||
return await sqlitePlugin.execute(options); |
|||
} catch (error) { |
|||
logger.error( |
|||
"Error in sqlite-execute:", |
|||
error, |
|||
JSON.stringify(error), |
|||
(error as any)?.stack, |
|||
); |
|||
throw error; |
|||
} |
|||
}); |
|||
|
|||
ipcMain.handle("sqlite-query", async (_event, options) => { |
|||
try { |
|||
return await sqlitePlugin.query(options); |
|||
} catch (error) { |
|||
logger.error( |
|||
"Error in sqlite-query:", |
|||
error, |
|||
JSON.stringify(error), |
|||
(error as any)?.stack, |
|||
); |
|||
throw error; |
|||
} |
|||
}); |
|||
|
|||
ipcMain.handle("sqlite-close-connection", async (_event, options) => { |
|||
try { |
|||
return await sqlitePlugin.closeConnection(options); |
|||
} catch (error) { |
|||
logger.error( |
|||
"Error in sqlite-close-connection:", |
|||
error, |
|||
JSON.stringify(error), |
|||
(error as any)?.stack, |
|||
); |
|||
throw error; |
|||
} |
|||
}); |
|||
|
|||
ipcMain.handle("sqlite-is-available", async () => { |
|||
return sqlitePlugin !== null; |
|||
}); |
@ -1,178 +0,0 @@ |
|||
const { contextBridge, ipcRenderer } = require("electron"); |
|||
|
|||
const logger = { |
|||
log: (message, ...args) => { |
|||
// Always log in development, log with context in production
|
|||
if (process.env.NODE_ENV !== "production") { |
|||
/* eslint-disable no-console */ |
|||
console.log(`[Electron Preload] ${message}`, ...args); |
|||
/* eslint-enable no-console */ |
|||
} |
|||
}, |
|||
warn: (message, ...args) => { |
|||
// Always log warnings
|
|||
/* eslint-disable no-console */ |
|||
console.warn(`[Electron Preload] ${message}`, ...args); |
|||
/* eslint-enable no-console */ |
|||
}, |
|||
error: (message, ...args) => { |
|||
// Always log errors
|
|||
/* eslint-disable no-console */ |
|||
console.error(`[Electron Preload] ${message}`, ...args); |
|||
/* eslint-enable no-console */ |
|||
}, |
|||
info: (message, ...args) => { |
|||
// Always log info in development, log with context in production
|
|||
if (process.env.NODE_ENV !== "production") { |
|||
/* eslint-disable no-console */ |
|||
console.info(`[Electron Preload] ${message}`, ...args); |
|||
/* eslint-enable no-console */ |
|||
} |
|||
}, |
|||
}; |
|||
|
|||
// Use a more direct path resolution approach
|
|||
const getPath = (pathType) => { |
|||
switch (pathType) { |
|||
case "userData": |
|||
return ( |
|||
process.env.APPDATA || |
|||
(process.platform === "darwin" |
|||
? `${process.env.HOME}/Library/Application Support` |
|||
: `${process.env.HOME}/.local/share`) |
|||
); |
|||
case "home": |
|||
return process.env.HOME; |
|||
case "appPath": |
|||
return process.resourcesPath; |
|||
default: |
|||
return ""; |
|||
} |
|||
}; |
|||
|
|||
logger.info("Preload script starting..."); |
|||
|
|||
// Force electron platform in the renderer process
|
|||
window.process = { env: { VITE_PLATFORM: "electron" } }; |
|||
|
|||
try { |
|||
contextBridge.exposeInMainWorld("electronAPI", { |
|||
// Path utilities
|
|||
getPath, |
|||
|
|||
// IPC functions
|
|||
send: (channel, data) => { |
|||
const validChannels = ["toMain"]; |
|||
if (validChannels.includes(channel)) { |
|||
ipcRenderer.send(channel, data); |
|||
} |
|||
}, |
|||
receive: (channel, func) => { |
|||
const validChannels = ["fromMain"]; |
|||
if (validChannels.includes(channel)) { |
|||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
|||
} |
|||
}, |
|||
// Environment info
|
|||
env: { |
|||
isElectron: true, |
|||
isDev: process.env.NODE_ENV === "development", |
|||
platform: "electron", // Explicitly set platform
|
|||
}, |
|||
// Path utilities
|
|||
getBasePath: () => { |
|||
return process.env.NODE_ENV === "development" ? "/" : "./"; |
|||
}, |
|||
}); |
|||
|
|||
// Create a proxy for the CapacitorSQLite plugin
|
|||
const createSQLiteProxy = () => { |
|||
const MAX_RETRIES = 3; |
|||
const RETRY_DELAY = 1000; // 1 second
|
|||
|
|||
const withRetry = async (operation, ...args) => { |
|||
let lastError; |
|||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { |
|||
try { |
|||
return await operation(...args); |
|||
} catch (error) { |
|||
lastError = error; |
|||
if (attempt < MAX_RETRIES) { |
|||
logger.warn( |
|||
`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) => { |
|||
return async (...args) => { |
|||
try { |
|||
return await withRetry( |
|||
ipcRenderer.invoke, |
|||
"sqlite-" + method, |
|||
...args, |
|||
); |
|||
} catch (error) { |
|||
logger.error(`SQLite ${method} failed:`, error); |
|||
throw new Error( |
|||
`Database operation failed: ${error.message || "Unknown error"}`, |
|||
); |
|||
} |
|||
}; |
|||
}; |
|||
|
|||
// Create a proxy that matches the CapacitorSQLite interface
|
|||
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"), |
|||
getPlatform: () => Promise.resolve("electron"), |
|||
// Add other methods as needed
|
|||
}; |
|||
}; |
|||
|
|||
// Expose only the CapacitorSQLite proxy
|
|||
contextBridge.exposeInMainWorld("electron", { |
|||
sqlite: createSQLiteProxy(), |
|||
getPath: (pathType) => ipcRenderer.invoke("get-path", pathType), |
|||
send: (channel, data) => { |
|||
ipcRenderer.send(channel, data); |
|||
}, |
|||
receive: (channel, func) => { |
|||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
|||
}, |
|||
ipcRenderer: { |
|||
on: (channel, func) => { |
|||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
|||
}, |
|||
once: (channel, func) => { |
|||
ipcRenderer.once(channel, (event, ...args) => func(...args)); |
|||
}, |
|||
send: (channel, data) => { |
|||
ipcRenderer.send(channel, data); |
|||
}, |
|||
invoke: (channel, ...args) => { |
|||
return ipcRenderer.invoke(channel, ...args); |
|||
} |
|||
}, |
|||
env: { |
|||
platform: "electron", |
|||
}, |
|||
getBasePath: () => ipcRenderer.invoke("get-base-path"), |
|||
}); |
|||
|
|||
logger.info("Preload script completed successfully"); |
|||
} catch (error) { |
|||
logger.error("Error in preload script:", error); |
|||
} |
Loading…
Reference in new issue