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
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user