diff --git a/package.json b/package.json
index 3ca41c86..0224fe64 100644
--- a/package.json
+++ b/package.json
@@ -22,11 +22,11 @@
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
"clean:electron": "rimraf dist-electron",
"build:pywebview": "vite build --config vite.config.pywebview.mts",
- "build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
+ "build:electron": "npm run clean:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
"build:web": "vite build --config vite.config.web.mts",
- "electron:dev": "npm run build && electron dist-electron",
- "electron:start": "electron dist-electron",
+ "electron:dev": "npm run build && electron .",
+ "electron:start": "electron .",
"clean:android": "adb uninstall app.timesafari.app || true",
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
@@ -169,13 +169,12 @@
},
"files": [
"dist-electron/**/*",
- "src/electron/**/*",
- "main.js"
+ "dist/**/*"
],
"extraResources": [
{
- "from": "dist-electron",
- "to": "."
+ "from": "dist",
+ "to": "www"
}
],
"linux": {
diff --git a/scripts/build-electron.js b/scripts/build-electron.js
index 2eb71575..677826f9 100644
--- a/scripts/build-electron.js
+++ b/scripts/build-electron.js
@@ -1,98 +1,243 @@
+const fs = require('fs');
const path = require('path');
-const fs = require('fs-extra');
-async function main() {
- try {
console.log('Starting electron build process...');
- // Create dist directory if it doesn't exist
- const distElectronDir = path.resolve(__dirname, '../dist-electron');
- await fs.ensureDir(distElectronDir);
-
// Copy web files
- const wwwDir = path.join(distElectronDir, 'www');
- await fs.ensureDir(wwwDir);
- await fs.copy('dist', wwwDir);
+const webDistPath = path.join(__dirname, '..', 'dist');
+const electronDistPath = path.join(__dirname, '..', 'dist-electron');
+const wwwPath = path.join(electronDistPath, 'www');
+
+// Create www directory if it doesn't exist
+if (!fs.existsSync(wwwPath)) {
+ fs.mkdirSync(wwwPath, { recursive: true });
+}
- // Copy and fix index.html
- const indexPath = path.join(wwwDir, 'index.html');
- let indexContent = await fs.readFile(indexPath, 'utf8');
+// Copy web files to www directory
+fs.cpSync(webDistPath, wwwPath, { recursive: true });
- // More comprehensive path fixing
+// Fix asset paths in index.html
+const indexPath = path.join(wwwPath, 'index.html');
+let indexContent = fs.readFileSync(indexPath, 'utf8');
+
+// Fix asset paths
indexContent = indexContent
- // Fix absolute paths to be relative
- .replace(/src="\//g, 'src="\./')
- .replace(/href="\//g, 'href="\./')
- // Fix modulepreload paths
- .replace(/]*rel="modulepreload"[^>]*href="\/assets\//g, ']*href="\.\/assets\//g, ']*href="\/assets\//g, ']*href="\.\/assets\//g, ' console.log(...args),
+ error: (...args) => console.error(...args),
+ info: (...args) => console.info(...args),
+ warn: (...args) => console.warn(...args),
+ debug: (...args) => console.debug(...args),
+};
+
+// 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
+ "", // 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, Google Fonts, and zxing-wasm
+ 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 https://*.jsdelivr.net;" +
+ "img-src 'self' data: https: blob:;" +
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.jsdelivr.net;" +
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
+ "font-src 'self' data: https://fonts.gstatic.com;" +
+ "style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
+ "worker-src 'self' blob:;",
+ ],
+ },
+ });
+ },
+ );
+
+ // 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();
+ }
+});
- console.log('Build completed successfully!');
- } catch (error) {
- console.error('Build failed:', error);
- process.exit(1);
+app.on("activate", () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
}
+});
+
+// Handle any errors
+process.on("uncaughtException", (error) => {
+ logger.error("Uncaught Exception:", error);
+});
+`;
+
+// Write the main process file
+const mainDest = path.join(electronDistPath, 'main.js');
+fs.writeFileSync(mainDest, mainContent);
+
+// Copy preload script if it exists
+const preloadSrc = path.join(__dirname, '..', 'src', 'electron', 'preload.js');
+const preloadDest = path.join(electronDistPath, 'preload.js');
+if (fs.existsSync(preloadSrc)) {
+ console.log(`Copying ${preloadSrc} to ${preloadDest}`);
+ fs.copyFileSync(preloadSrc, preloadDest);
}
-main();
\ No newline at end of file
+// Verify build structure
+console.log('\nVerifying build structure:');
+console.log('Files in dist-electron:', fs.readdirSync(electronDistPath));
+
+console.log('Build completed successfully!');
\ No newline at end of file
diff --git a/src/electron/main.ts b/src/electron/main.ts
new file mode 100644
index 00000000..80804f90
--- /dev/null
+++ b/src/electron/main.ts
@@ -0,0 +1,183 @@
+import { app, BrowserWindow } from "electron";
+import path from "path";
+import fs from "fs";
+
+// Simple logger implementation
+const logger = {
+ log: (...args: unknown[]) => console.log(...args),
+ error: (...args: unknown[]) => console.error(...args),
+ info: (...args: unknown[]) => console.info(...args),
+ warn: (...args: unknown[]) => console.warn(...args),
+ debug: (...args: unknown[]) => console.debug(...args),
+};
+
+// Check if running in dev mode
+const isDev = process.argv.includes("--inspect");
+
+function createWindow(): void {
+ // 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
+ "", // 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);
+});
+
\ No newline at end of file
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index 95e3b480..fcbd2cf4 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -3,7 +3,7 @@ import { logToDb } from "../db";
function safeStringify(obj: unknown) {
const seen = new WeakSet();
- return JSON.stringify(obj, (key, value) => {
+ return JSON.stringify(obj, (_key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "[Circular]";
@@ -69,3 +69,11 @@ export const logger = {
logToDb(message + argsString);
},
};
+
+// Add CommonJS export for Electron
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = { logger };
+}
+
+// Add default export for ESM
+export default { logger };
diff --git a/tsconfig.electron.json b/tsconfig.electron.json
new file mode 100644
index 00000000..a2277a1c
--- /dev/null
+++ b/tsconfig.electron.json
@@ -0,0 +1,26 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "target": "ES2020",
+ "outDir": "dist-electron",
+ "rootDir": "src",
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "allowJs": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "allowImportingTsExtensions": true,
+ "types": ["vite/client"],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "src/electron/**/*.ts",
+ "src/utils/**/*.ts",
+ "src/constants/**/*.ts"
+ ]
+}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
index 6aa7b5c6..f2bdbb1d 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -4,7 +4,9 @@
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true
+ "allowSyntheticDefaultImports": true,
+ "allowImportingTsExtensions": true,
+ "noEmit": true
},
"include": ["vite.config.*"]
}
\ No newline at end of file
diff --git a/vite.config.electron.mts b/vite.config.electron.mts
index b83e380d..cb7342c0 100644
--- a/vite.config.electron.mts
+++ b/vite.config.electron.mts
@@ -1,11 +1,58 @@
import { defineConfig, mergeConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts";
+import path from 'path';
export default defineConfig(async () => {
const baseConfig = await createBuildConfig('electron');
return mergeConfig(baseConfig, {
- plugins: [{
+ build: {
+ outDir: 'dist-electron',
+ rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, 'src/electron/main.ts'),
+ preload: path.resolve(__dirname, 'src/electron/preload.js'),
+ },
+ external: ['electron'],
+ output: {
+ format: 'cjs',
+ entryFileNames: '[name].js',
+ assetFileNames: 'assets/[name].[ext]',
+ },
+ },
+ target: 'node18',
+ minify: false,
+ sourcemap: true,
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ },
+ },
+ optimizeDeps: {
+ include: ['@/utils/logger']
+ },
+ plugins: [
+ {
+ name: 'typescript-transform',
+ transform(code: string, id: string) {
+ if (id.endsWith('main.ts')) {
+ // Replace the logger import with inline logger
+ return code.replace(
+ /import\s*{\s*logger\s*}\s*from\s*['"]@\/utils\/logger['"];?/,
+ `const logger = {
+ log: (...args) => console.log(...args),
+ error: (...args) => console.error(...args),
+ info: (...args) => console.info(...args),
+ warn: (...args) => console.warn(...args),
+ debug: (...args) => console.debug(...args),
+ };`
+ );
+ }
+ return code;
+ }
+ },
+ {
name: 'remove-sw-imports',
transform(code: string, id: string) {
if (
@@ -24,6 +71,12 @@ export default defineConfig(async () => {
};
}
}
- }]
+ }
+ ],
+ ssr: {
+ noExternal: ['@/utils/logger']
+ },
+ base: './',
+ publicDir: 'public',
});
});
\ No newline at end of file