Browse Source

refactor: remove electron preload script and update database handling

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 process
sql-absurd-sql-further
Matthew Raymer 5 days ago
parent
commit
8b215c909d
  1. 48
      experiment.sh
  2. 174
      src/electron/main.js
  3. 712
      src/electron/main.ts
  4. 178
      src/electron/preload.js
  5. 1
      vite.config.electron.mts

48
experiment.sh

@ -145,40 +145,40 @@ if ! measure_time ./node_modules/.bin/tsc -p tsconfig.electron.json; then
fi
# Build electron main process
log_info "Building electron main process..."
if ! measure_time env VITE_GIT_HASH="$GIT_HASH" npx vite build --config vite.config.electron.mts --mode electron; then
log_error "Electron main process build failed!"
exit 4
fi
# log_info "Building electron main process..."
# if ! measure_time env VITE_GIT_HASH="$GIT_HASH" npx vite build --config vite.config.electron.mts --mode electron; then
# log_error "Electron main process build failed!"
# exit 4
# fi
# Organize files
log_info "Organizing build artifacts..."
mkdir -p dist-electron/www
cp -r dist/* dist-electron/www/ || log_error "Failed to copy web assets"
mkdir -p dist-electron/resources
cp src/electron/preload.js dist-electron/resources/preload.js || log_error "Failed to copy preload script"
# log_info "Organizing build artifacts..."
#mkdir -p dist-electron/www
#cp -r dist/* dist-electron/www/ || log_error "Failed to copy web assets"
#mkdir -p dist-electron/resources
#cp src/electron/preload.js dist-electron/resources/preload.js || log_error "Failed to copy preload script"
# Build the AppImage
log_info "Building AppImage package..."
if ! measure_time npx electron-builder --linux AppImage; then
log_error "AppImage build failed!"
exit 5
fi
#log_info "Building AppImage package..."
#if ! measure_time npx electron-builder --linux AppImage; then
# log_error "AppImage build failed!"
# exit 5
#fi
# Print build summary
echo -e "\n${GREEN}=== Build Summary ===${NC}"
# echo -e "\n${GREEN}=== Build Summary ===${NC}"
log_success "Build completed successfully!"
log_info "Build artifacts location: $(pwd)/dist-electron"
log_info "AppImage location: $(find_appimage)"
# log_info "Build artifacts location: $(pwd)/dist-electron"
# log_info "AppImage location: $(find_appimage)"
# Check for build warnings
if grep -q "default Electron icon is used" dist-electron-packages/builder-effective-config.yaml; then
log_warn "Using default Electron icon - consider adding a custom icon"
fi
# if grep -q "default Electron icon is used" dist-electron-packages/builder-effective-config.yaml; then
# log_warn "Using default Electron icon - consider adding a custom icon"
# fi
if grep -q "chunks are larger than 1000 kB" dist-electron-packages/builder-effective-config.yaml; then
log_warn "Large chunks detected - consider implementing code splitting"
fi
# if grep -q "chunks are larger than 1000 kB" dist-electron-packages/builder-effective-config.yaml; then
# log_warn "Large chunks detected - consider implementing code splitting"
# fi
echo -e "\n${GREEN}=== End of Build Process ===${NC}\n"

174
src/electron/main.js

@ -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);
});

712
src/electron/main.ts

@ -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;
});

178
src/electron/preload.js

@ -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);
}

1
vite.config.electron.mts

@ -7,7 +7,6 @@ export default defineConfig({
rollupOptions: {
input: {
main: path.resolve(__dirname, 'src/electron/main.ts'),
preload: path.resolve(__dirname, 'src/electron/preload.js'),
},
external: [
// Node.js built-ins

Loading…
Cancel
Save