Make Electron build
I historically have never liked Electron, and I don't like it now, but unfortunately due to poor browser support, I don't have a choice to ship anything more lightweight if I want to use the Filesystem API as part of the packaged build, which is kind of the point. I guess it's true: "You either die a hero or you live long enough to become the villain." This is an experimental build.
This commit is contained in:
48
.github/workflows/build.yml
vendored
48
.github/workflows/build.yml
vendored
@@ -2,7 +2,7 @@ name: Build and test project
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, develop]
|
branches: [main, develop, electron]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -94,3 +94,49 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd tests/e2e
|
cd tests/e2e
|
||||||
./run-e2e.sh
|
./run-e2e.sh
|
||||||
|
|
||||||
|
build-electron:
|
||||||
|
needs: build-and-test
|
||||||
|
if: always() && needs.build-and-test.result == 'success'
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
- os: macos-latest
|
||||||
|
platform: mac
|
||||||
|
- os: windows-latest
|
||||||
|
platform: win
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Build Electron app
|
||||||
|
run: |
|
||||||
|
cd electron
|
||||||
|
npm install
|
||||||
|
npm run build:${{ matrix.platform }}
|
||||||
|
|
||||||
|
- name: Upload Electron artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: electron-${{ matrix.platform }}
|
||||||
|
path: |
|
||||||
|
electron/release/*.AppImage
|
||||||
|
electron/release/*.dmg
|
||||||
|
electron/release/*.zip
|
||||||
|
electron/release/*.exe
|
||||||
|
if-no-files-found: warn
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -22,6 +22,11 @@ web/dist/
|
|||||||
|
|
||||||
# Node
|
# Node
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
|
electron/node_modules/
|
||||||
|
|
||||||
|
# Electron build artifacts
|
||||||
|
electron/dist/
|
||||||
|
electron/release/
|
||||||
|
|
||||||
# Cached test assets (firmware, KOReader zips)
|
# Cached test assets (firmware, KOReader zips)
|
||||||
tests/cached_assets/
|
tests/cached_assets/
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -109,6 +109,11 @@ tests/
|
|||||||
playwright.config.js
|
playwright.config.js
|
||||||
run-e2e.sh
|
run-e2e.sh
|
||||||
|
|
||||||
|
electron/
|
||||||
|
main.js # Electron main process (local HTTP server + BrowserWindow)
|
||||||
|
package.json # electron, electron-builder, build config
|
||||||
|
dist/ # Copied web/dist/ + packaged installers (gitignored)
|
||||||
|
|
||||||
# Root scripts
|
# Root scripts
|
||||||
test.sh # Runs all tests (WASM + E2E)
|
test.sh # Runs all tests (WASM + E2E)
|
||||||
serve-locally.sh # Serves app at localhost:8888
|
serve-locally.sh # Serves app at localhost:8888
|
||||||
@@ -201,6 +206,21 @@ To automatically rebuild when source files change:
|
|||||||
./serve-locally.sh --dev
|
./serve-locally.sh --dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Building the Electron app
|
||||||
|
|
||||||
|
A standalone desktop app can be built from the `electron/` directory. It packages the built `web/dist/` output into an Electron shell with a local HTTP server (avoids `file://` limitations).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd electron
|
||||||
|
npm install
|
||||||
|
npm run build:linux # Linux AppImage
|
||||||
|
npm run build:mac # macOS DMG
|
||||||
|
npm run build:win # Windows NSIS installer
|
||||||
|
npm run build # all platforms
|
||||||
|
```
|
||||||
|
|
||||||
|
Each platform script first copies `web/dist/` into `electron/dist/`, then runs electron-builder. Output goes to `electron/dist/`.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run all tests (WASM integration + E2E):
|
Run all tests (WASM integration + E2E):
|
||||||
|
|||||||
16
electron/copy-dist.mjs
Normal file
16
electron/copy-dist.mjs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { cpSync, rmSync, existsSync } from 'node:fs';
|
||||||
|
import { join, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const dir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const src = join(dir, '..', 'web', 'dist');
|
||||||
|
const dst = join(dir, 'dist');
|
||||||
|
|
||||||
|
if (!existsSync(src)) {
|
||||||
|
console.error('web/dist/ does not exist — run "cd web && npm run build" first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
rmSync(dst, { recursive: true, force: true });
|
||||||
|
cpSync(src, dst, { recursive: true });
|
||||||
|
console.log(`Copied ${src} -> ${dst}`);
|
||||||
76
electron/main.js
Normal file
76
electron/main.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
const { app, BrowserWindow, shell } = require('electron');
|
||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const DIST = path.join(__dirname, 'dist');
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.wasm': 'application/wasm',
|
||||||
|
'.zip': 'application/zip',
|
||||||
|
'.tgz': 'application/gzip',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.webmanifest': 'application/manifest+json',
|
||||||
|
};
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let win;
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
win = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 900,
|
||||||
|
icon: path.join(DIST, 'favicon', 'favicon-96x96.png'),
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
webPreferences: { contextIsolation: true },
|
||||||
|
});
|
||||||
|
const localOrigin = `http://localhost:${server.address().port}`;
|
||||||
|
|
||||||
|
win.loadURL(localOrigin);
|
||||||
|
win.on('closed', () => { win = null; });
|
||||||
|
|
||||||
|
const isExternal = (url) => {
|
||||||
|
try { return new URL(url).origin !== localOrigin; } catch { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
win.webContents.on('will-navigate', (e, url) => {
|
||||||
|
if (isExternal(url)) { e.preventDefault(); shell.openExternal(url); }
|
||||||
|
});
|
||||||
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
if (isExternal(url)) shell.openExternal(url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
server = http.createServer((req, res) => {
|
||||||
|
const url = new URL(req.url, 'http://localhost');
|
||||||
|
let filePath = path.join(DIST, decodeURIComponent(url.pathname));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.isDirectory()) filePath = path.join(filePath, 'index.html');
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.statSync(filePath);
|
||||||
|
res.writeHead(200, { 'Content-Type': MIME[path.extname(filePath)] || 'application/octet-stream' });
|
||||||
|
fs.createReadStream(filePath).pipe(res);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('Not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
app.whenReady().then(createWindow);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => { app.quit(); });
|
||||||
|
|
||||||
|
app.on('before-quit', () => { server.close(); });
|
||||||
4895
electron/package-lock.json
generated
Normal file
4895
electron/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
electron/package.json
Normal file
32
electron/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "kobopatch-electron",
|
||||||
|
"version": "1.5.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"copy-dist": "node copy-dist.mjs",
|
||||||
|
"start": "npm run copy-dist && electron .",
|
||||||
|
"build": "npm run copy-dist && electron-builder",
|
||||||
|
"build:mac": "npm run copy-dist && electron-builder --mac",
|
||||||
|
"build:win": "npm run copy-dist && electron-builder --win",
|
||||||
|
"build:linux": "npm run copy-dist && electron-builder --linux"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^35.0.0",
|
||||||
|
"electron-builder": "^26.0.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "be.nicoverbruggen.kobopatch-webui",
|
||||||
|
"productName": "KoboPatch Web UI",
|
||||||
|
"directories": {
|
||||||
|
"output": "release"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"main.js",
|
||||||
|
"dist/**/*"
|
||||||
|
],
|
||||||
|
"mac": { "target": "dmg", "icon": "dist/favicon/web-app-manifest-512x512.png" },
|
||||||
|
"win": { "target": "portable", "icon": "dist/favicon/web-app-manifest-512x512.png" },
|
||||||
|
"linux": { "target": "AppImage", "icon": "dist/favicon/web-app-manifest-512x512.png" }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user