Files
next-ai-draw-io/electron/main/next-server.ts

162 lines
4.5 KiB
TypeScript
Raw Permalink Normal View History

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>
2025-12-22 09:18:21 +08:00
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()
}