|
1 | 1 | /* eslint-disable no-undef */ |
2 | | -const { app, BrowserWindow, ipcMain, Notification, nativeTheme, Menu } = require('electron'); |
| 2 | +const { app, BrowserWindow, ipcMain, Notification, nativeTheme, Menu, protocol, net } = require('electron'); |
3 | 3 | const path = require('path'); |
| 4 | +const fs = require('fs'); |
| 5 | +const { pathToFileURL } = require('url'); |
| 6 | + |
| 7 | +// Register custom protocol scheme before app is ready |
| 8 | +// This allows serving the Expo web export with absolute paths (/_expo/static/...) |
| 9 | +// via a custom protocol instead of file://, which breaks absolute path resolution. |
| 10 | +protocol.registerSchemesAsPrivileged([ |
| 11 | + { |
| 12 | + scheme: 'app', |
| 13 | + privileges: { |
| 14 | + standard: true, |
| 15 | + secure: true, |
| 16 | + supportFetchAPI: true, |
| 17 | + corsEnabled: true, |
| 18 | + stream: true, |
| 19 | + }, |
| 20 | + }, |
| 21 | +]); |
4 | 22 |
|
5 | 23 | // Handle creating/removing shortcuts on Windows when installing/uninstalling. |
6 | 24 | if (require('electron-squirrel-startup')) { |
@@ -33,9 +51,14 @@ function createWindow() { |
33 | 51 | }); |
34 | 52 |
|
35 | 53 | // Load the app |
36 | | - const startUrl = isDev ? 'http://localhost:8081' : `file://${path.join(__dirname, '../dist/index.html')}`; |
37 | | - |
38 | | - mainWindow.loadURL(startUrl); |
| 54 | + if (isDev) { |
| 55 | + // In development, load from the Expo dev server |
| 56 | + mainWindow.loadURL('http://localhost:8081'); |
| 57 | + } else { |
| 58 | + // In production, load via the custom app:// protocol |
| 59 | + // which correctly resolves absolute paths (/_expo/static/...) from the dist directory |
| 60 | + mainWindow.loadURL('app://bundle/index.html'); |
| 61 | + } |
39 | 62 |
|
40 | 63 | // Show window when ready |
41 | 64 | mainWindow.once('ready-to-show', () => { |
@@ -154,6 +177,41 @@ ipcMain.handle('get-platform', () => { |
154 | 177 |
|
155 | 178 | // Handle app ready |
156 | 179 | app.whenReady().then(() => { |
| 180 | + // Register custom protocol handler for serving the Expo web export |
| 181 | + // This resolves absolute paths like /_expo/static/js/... from the dist directory |
| 182 | + const distPath = path.join(__dirname, '..', 'dist'); |
| 183 | + const resolvedDist = path.resolve(distPath); |
| 184 | + |
| 185 | + protocol.handle('app', (request) => { |
| 186 | + const url = new URL(request.url); |
| 187 | + // Decode the pathname, join with base path, then canonicalize to prevent directory traversal |
| 188 | + const joinedPath = path.join(distPath, decodeURIComponent(url.pathname)); |
| 189 | + const resolvedPath = path.resolve(joinedPath); |
| 190 | + |
| 191 | + // Security check: ensure resolved path is within distPath to prevent directory traversal |
| 192 | + let filePath; |
| 193 | + if (!resolvedPath.startsWith(resolvedDist + path.sep) && resolvedPath !== resolvedDist) { |
| 194 | + // Path escapes distPath - fall back to index.html |
| 195 | + filePath = path.join(resolvedDist, 'index.html'); |
| 196 | + } else { |
| 197 | + filePath = resolvedPath; |
| 198 | + |
| 199 | + // If the path points to a directory or file doesn't exist, fall back to index.html |
| 200 | + // This supports SPA client-side routing |
| 201 | + try { |
| 202 | + const stat = fs.statSync(filePath); |
| 203 | + if (stat.isDirectory()) { |
| 204 | + filePath = path.join(resolvedDist, 'index.html'); |
| 205 | + } |
| 206 | + } catch { |
| 207 | + // File not found - serve index.html for client-side routing |
| 208 | + filePath = path.join(resolvedDist, 'index.html'); |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + return net.fetch(pathToFileURL(filePath).toString()); |
| 213 | + }); |
| 214 | + |
157 | 215 | createMenu(); |
158 | 216 | createWindow(); |
159 | 217 |
|
|
0 commit comments