mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
feat(electron): add desktop application support with electron (#344)
* feat(electron): add desktop application support with electron - implement complete Electron main process architecture with window management, app menu, IPC handlers, and settings window - integrate Next.js server for production builds with embedded standalone server - add configuration management with persistent storage and env file support - create preload scripts with secure context bridge for renderer communication - set up electron-builder configuration for multi-platform packaging (macOS, Windows, Linux) - add GitHub Actions workflow for automated release builds - include development scripts for hot-reload during Electron development * feat(electron): enhance security and stability - encrypt API keys using Electron safeStorage API before persisting to disk - add error handling and rollback for preset switching failures - extract inline styles to external CSS file and remove unsafe-inline from CSP - implement dynamic port allocation with automatic fallback for production builds * fix(electron): add maintainer field for Linux .deb package - add maintainer email to linux configuration in electron-builder.yml - required for building .deb packages * fix(electron): use shx for cross-platform file copying - replace Unix-only cp -r with npx shx cp -r - add shx as devDependency for Windows compatibility * fix(electron): fix runtime icon path for all platforms - use icon.png directly instead of platform-specific formats - electron-builder handles icon conversion during packaging - macOS uses embedded icon from app bundle, no explicit path needed - add icon.png to extraResources for Windows/Linux runtime access * fix(electron): add security warning for plaintext API key storage - warn user when safeStorage is unavailable (Linux without keyring) - fail secure: throw error if encryption fails instead of storing plaintext - prevent duplicate warnings with hasWarnedAboutPlaintext flag * fix(electron): add remaining review fixes - Add Windows ARM64 architecture support - Add IPC input validation with config key whitelist - Add server.js existence check before starting Next.js server - Make afterPack throw error on missing directories - Add workflow permissions for release job --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
This commit is contained in:
161
electron/main/next-server.ts
Normal file
161
electron/main/next-server.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { existsSync } from "node:fs"
|
||||
import path from "node:path"
|
||||
import { app, type UtilityProcess, utilityProcess } from "electron"
|
||||
import {
|
||||
findAvailablePort,
|
||||
getAllocatedPort,
|
||||
getServerUrl,
|
||||
isPortAvailable,
|
||||
} from "./port-manager"
|
||||
|
||||
let serverProcess: UtilityProcess | null = null
|
||||
|
||||
/**
|
||||
* Get the path to the standalone server resources
|
||||
* In packaged app: resources/standalone
|
||||
* In development: .next/standalone
|
||||
*/
|
||||
function getResourcePath(): string {
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, "standalone")
|
||||
}
|
||||
return path.join(app.getAppPath(), ".next", "standalone")
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the server to be ready by polling the health endpoint
|
||||
*/
|
||||
async function waitForServer(url: string, timeout = 30000): Promise<void> {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (response.ok || response.status < 500) {
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Server not ready yet
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
throw new Error(`Server startup timeout after ${timeout}ms`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Next.js standalone server using Electron's utilityProcess
|
||||
* This API is designed for running Node.js code in the background
|
||||
*/
|
||||
export async function startNextServer(): Promise<string> {
|
||||
const resourcePath = getResourcePath()
|
||||
const serverPath = path.join(resourcePath, "server.js")
|
||||
|
||||
console.log(`Starting Next.js server from: ${resourcePath}`)
|
||||
console.log(`Server script path: ${serverPath}`)
|
||||
|
||||
// Verify server script exists before attempting to start
|
||||
if (!existsSync(serverPath)) {
|
||||
throw new Error(
|
||||
`Server script not found at ${serverPath}. ` +
|
||||
"Please ensure the app was built correctly with 'npm run build'.",
|
||||
)
|
||||
}
|
||||
|
||||
// Find an available port (random in production, fixed in development)
|
||||
const port = await findAvailablePort()
|
||||
console.log(`Using port: ${port}`)
|
||||
|
||||
// Set up environment variables
|
||||
const env: Record<string, string> = {
|
||||
NODE_ENV: "production",
|
||||
PORT: String(port),
|
||||
HOSTNAME: "localhost",
|
||||
}
|
||||
|
||||
// Set cache directory to a writable location (user's app data folder)
|
||||
// This is necessary because the packaged app might be on a read-only volume
|
||||
if (app.isPackaged) {
|
||||
const cacheDir = path.join(app.getPath("userData"), "cache")
|
||||
env.NEXT_CACHE_DIR = cacheDir
|
||||
}
|
||||
|
||||
// Copy existing environment variables
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value !== undefined && !env[key]) {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Use Electron's utilityProcess API for running Node.js in background
|
||||
// This is the recommended way to run Node.js code in Electron
|
||||
serverProcess = utilityProcess.fork(serverPath, [], {
|
||||
cwd: resourcePath,
|
||||
env,
|
||||
stdio: "pipe",
|
||||
})
|
||||
|
||||
serverProcess.stdout?.on("data", (data) => {
|
||||
console.log(`[Next.js] ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
serverProcess.stderr?.on("data", (data) => {
|
||||
console.error(`[Next.js Error] ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
serverProcess.on("exit", (code) => {
|
||||
console.log(`Next.js server exited with code ${code}`)
|
||||
serverProcess = null
|
||||
})
|
||||
|
||||
const url = getServerUrl()
|
||||
await waitForServer(url)
|
||||
console.log(`Next.js server started at ${url}`)
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the Next.js server process
|
||||
*/
|
||||
export function stopNextServer(): void {
|
||||
if (serverProcess) {
|
||||
console.log("Stopping Next.js server...")
|
||||
serverProcess.kill()
|
||||
serverProcess = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the server to fully stop
|
||||
*/
|
||||
async function waitForServerStop(timeout = 5000): Promise<void> {
|
||||
const port = getAllocatedPort()
|
||||
if (port === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
const available = await isPortAvailable(port)
|
||||
if (available) {
|
||||
return
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
console.warn("Server stop timeout, port may still be in use")
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the Next.js server with new environment variables
|
||||
*/
|
||||
export async function restartNextServer(): Promise<string> {
|
||||
console.log("Restarting Next.js server...")
|
||||
|
||||
// Stop the current server
|
||||
stopNextServer()
|
||||
|
||||
// Wait for the port to be released
|
||||
await waitForServerStop()
|
||||
|
||||
// Start the server again
|
||||
return startNextServer()
|
||||
}
|
||||
Reference in New Issue
Block a user