From cb1d97943172823b59e692c4e3b644eb1a9e984b Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 1 May 2025 09:30:02 +0000 Subject: [PATCH] refactor(electron): improve build process and configuration - Enhance electron build configuration with proper asset handling - Add comprehensive logging and error tracking - Implement CSP headers for security - Fix module exports for logger compatibility - Update TypeScript and Vite configs for better build support - Improve development workflow with better dev tools integration --- package.json | 13 +- scripts/build-electron.js | 303 ++++++++++++++++++++++++++++---------- src/electron/main.ts | 183 +++++++++++++++++++++++ src/utils/logger.ts | 10 +- tsconfig.electron.json | 26 ++++ tsconfig.node.json | 4 +- vite.config.electron.mts | 57 ++++++- 7 files changed, 506 insertions(+), 90 deletions(-) create mode 100644 src/electron/main.ts create mode 100644 tsconfig.electron.json 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