-
Notifications
You must be signed in to change notification settings - Fork 23
Add Electron desktop app for Windows #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
1437041
3a1bc27
3740de2
816f3d8
89357cf
02658ea
62243e1
66d2fdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| name: Electron Windows Build | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - 'client/**' | ||
|
|
||
| jobs: | ||
| build-windows: | ||
| runs-on: windows-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 10 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| cache: pnpm | ||
| cache-dependency-path: client/pnpm-lock.yaml | ||
|
|
||
| - name: Install dependencies | ||
| working-directory: client | ||
| run: pnpm install | ||
|
|
||
| - name: Build Electron TypeScript | ||
| working-directory: client | ||
| run: npx tsc -p electron/tsconfig.json | ||
|
|
||
| - name: Build Vite | ||
| working-directory: client | ||
| run: npx vite build | ||
|
|
||
| - name: Build Windows executable | ||
| working-directory: client | ||
| run: npx electron-builder --win --config.win.target=dir | ||
|
|
||
| - name: Upload Windows build | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: loot-survivor-2-win-x64 | ||
| path: client/release/win-unpacked/ | ||
| retention-days: 30 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,3 +22,7 @@ tsconfig.tsbuildinfo | |
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
|
|
||
| # electron | ||
| /electron-dist | ||
| /release | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { app, BrowserWindow } from "electron"; | ||
| import http from "http"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
|
|
||
| const ALLOWED_PATH_PREFIX = "/trials"; | ||
|
|
||
| function getMimeType(filePath: string): string { | ||
| const ext = path.extname(filePath).toLowerCase(); | ||
| const types: Record<string, string> = { | ||
| ".html": "text/html", | ||
| ".js": "application/javascript", | ||
| ".css": "text/css", | ||
| ".json": "application/json", | ||
| ".png": "image/png", | ||
| ".jpg": "image/jpeg", | ||
| ".jpeg": "image/jpeg", | ||
| ".gif": "image/gif", | ||
| ".svg": "image/svg+xml", | ||
| ".ico": "image/x-icon", | ||
| ".wasm": "application/wasm", | ||
| ".woff": "font/woff", | ||
| ".woff2": "font/woff2", | ||
| ".ttf": "font/ttf", | ||
| ".mp3": "audio/mpeg", | ||
| ".ogg": "audio/ogg", | ||
| ".wav": "audio/wav", | ||
| ".webp": "image/webp", | ||
| ".webm": "video/webm", | ||
| ".mp4": "video/mp4", | ||
| }; | ||
| return types[ext] || "application/octet-stream"; | ||
| } | ||
|
|
||
| function startLocalServer(distPath: string): Promise<number> { | ||
| return new Promise((resolve) => { | ||
| const server = http.createServer((req, res) => { | ||
| const url = new URL(req.url || "/", "http://localhost"); | ||
| let filePath = path.join(distPath, url.pathname); | ||
|
|
||
| // Serve index.html for SPA routes | ||
| if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) { | ||
| filePath = path.join(distPath, "index.html"); | ||
| } | ||
|
|
||
| try { | ||
| const data = fs.readFileSync(filePath); | ||
| res.writeHead(200, { "Content-Type": getMimeType(filePath) }); | ||
| res.end(data); | ||
| } catch { | ||
| res.writeHead(404); | ||
| res.end("Not found"); | ||
| } | ||
| }); | ||
|
|
||
| server.listen(0, "127.0.0.1", () => { | ||
| const addr = server.address(); | ||
| const port = typeof addr === "object" && addr ? addr.port : 0; | ||
| resolve(port); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| async function createWindow() { | ||
| const win = new BrowserWindow({ | ||
| width: 1440, | ||
| height: 900, | ||
| title: "Loot Survivor 2", | ||
| webPreferences: { | ||
| contextIsolation: true, | ||
| nodeIntegration: false, | ||
| preload: path.join(__dirname, "preload.js"), | ||
| }, | ||
| }); | ||
|
|
||
| // Block navigation to routes outside /trials | ||
| win.webContents.on("will-navigate", (event, url) => { | ||
| try { | ||
| const parsed = new URL(url); | ||
| if ( | ||
| !parsed.pathname.startsWith(ALLOWED_PATH_PREFIX) && | ||
| parsed.pathname !== "/" | ||
| ) { | ||
| event.preventDefault(); | ||
| } | ||
| } catch { | ||
| event.preventDefault(); | ||
| } | ||
|
Comment on lines
+86
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }); | ||
|
|
||
| const isDev = !app.isPackaged; | ||
|
|
||
| if (isDev) { | ||
| await win.loadURL(`http://localhost:5173${ALLOWED_PATH_PREFIX}`); | ||
| } else { | ||
| const distPath = path.join(app.getAppPath(), "dist"); | ||
| const port = await startLocalServer(distPath); | ||
| await win.loadURL(`http://127.0.0.1:${port}${ALLOWED_PATH_PREFIX}`); | ||
| } | ||
| } | ||
|
|
||
| app.whenReady().then(createWindow).catch((err) => { | ||
| console.error("Failed to create window:", err); | ||
| app.quit(); | ||
| }); | ||
|
|
||
| app.on("window-all-closed", () => { | ||
| if (process.platform !== "darwin") { | ||
| app.quit(); | ||
| } | ||
| }); | ||
|
|
||
| app.on("activate", () => { | ||
| if (BrowserWindow.getAllWindows().length === 0) { | ||
| createWindow(); | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { contextBridge } from "electron"; | ||
|
|
||
| contextBridge.exposeInMainWorld("electronAPI", { | ||
| isElectron: true, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "es2020", | ||
| "module": "commonjs", | ||
| "outDir": "../electron-dist", | ||
| "rootDir": ".", | ||
| "strict": true, | ||
| "typeRoots": ["../node_modules/@types"], | ||
| "types": ["node"], | ||
| "esModuleInterop": true, | ||
| "skipLibCheck": true, | ||
| "forceConsistentCasingInFileNames": true | ||
| }, | ||
| "include": ["./**/*.ts"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,40 @@ | |
| "private": true, | ||
| "version": "0.0.0", | ||
| "type": "module", | ||
| "main": "electron-dist/main.js", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "tsc -b && vite build", | ||
| "lint": "eslint .", | ||
| "preview": "vite preview", | ||
| "serve": "vite preview" | ||
| "serve": "vite preview", | ||
| "electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && NODE_ENV=development electron .\"", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cross-platform compatibility issue with The inline environment variable syntax Suggested fix using cross-envAdd -"electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && NODE_ENV=development electron .\""
+"electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_ENV=development electron .\""🤖 Prompt for AI Agents |
||
| "electron:build": "tsc -p electron/tsconfig.json && vite build && electron-builder", | ||
| "electron:preview": "tsc -p electron/tsconfig.json && vite build && electron ." | ||
| }, | ||
| "build": { | ||
| "appId": "io.lootsurvivor.desktop", | ||
| "productName": "Loot Survivor 2", | ||
| "directories": { | ||
| "output": "release" | ||
| }, | ||
| "files": [ | ||
| "dist/**/*", | ||
| "electron-dist/**/*" | ||
| ], | ||
| "win": { | ||
| "target": [ | ||
| { | ||
| "target": "dir", | ||
| "arch": [ | ||
| "x64" | ||
| ] | ||
| } | ||
| ] | ||
| }, | ||
| "extraMetadata": { | ||
| "type": "commonjs" | ||
| } | ||
| }, | ||
| "dependencies": { | ||
| "@cartridge/connector": "^0.10.1", | ||
|
|
@@ -53,17 +81,29 @@ | |
| "vite-plugin-wasm": "^3.4.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^25.5.0", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| "@types/react": "^18.2.0", | ||
| "@types/react-dom": "^18.2.0", | ||
| "@types/react-lazy-load-image-component": "^1.6.4", | ||
| "@vitejs/plugin-react": "^4.3.4", | ||
| "concurrently": "^9.1.2", | ||
| "electron": "^34.2.0", | ||
| "electron-builder": "^25.1.8", | ||
| "eslint": "^9.24.0", | ||
| "eslint-plugin-react-hooks": "^5.2.0", | ||
| "eslint-plugin-react-refresh": "^0.4.19", | ||
| "typescript": "^5.8.3", | ||
| "typescript-eslint": "^8.30.1", | ||
| "vite": "^5.4.18", | ||
| "vite-plugin-top-level-await": "^1.5.0", | ||
| "wait-on": "^8.0.3", | ||
| "zustand": "^4.5.6" | ||
| }, | ||
| "pnpm": { | ||
| "onlyBuiltDependencies": [ | ||
| "electron", | ||
| "esbuild", | ||
| "@swc/core" | ||
| ] | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The local server is using synchronous file system calls (
existsSync,statSync,readFileSync) within the request handler. This will block the Electron main process for every file request, which can lead to unresponsiveness of the application, especially when loading many assets. It's better to use the asynchronous versions of these methods fromfs.promisesand make the request handlerasync.