forked from imbytecat/fullstack-starter
176 lines
3.7 KiB
TypeScript
176 lines
3.7 KiB
TypeScript
import { join } from 'node:path'
|
|
import { app, BrowserWindow, dialog, shell } from 'electron'
|
|
import { createSidecarRuntime } from './sidecar'
|
|
|
|
const DEV_SERVER_URL = 'http://localhost:3000'
|
|
const SAFE_EXTERNAL_PROTOCOLS = new Set(['https:', 'http:', 'mailto:'])
|
|
|
|
let mainWindow: BrowserWindow | null = null
|
|
let windowCreationPromise: Promise<void> | null = null
|
|
let isQuitting = false
|
|
|
|
const showErrorAndQuit = (title: string, detail: string) => {
|
|
if (isQuitting) {
|
|
return
|
|
}
|
|
|
|
dialog.showErrorBox(title, detail)
|
|
app.quit()
|
|
}
|
|
|
|
const sidecar = createSidecarRuntime({
|
|
devServerUrl: DEV_SERVER_URL,
|
|
isPackaged: app.isPackaged,
|
|
resourcesPath: process.resourcesPath,
|
|
isQuitting: () => isQuitting,
|
|
onUnexpectedStop: (detail) => {
|
|
showErrorAndQuit('Service Stopped', detail)
|
|
},
|
|
})
|
|
|
|
const toErrorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error))
|
|
|
|
const canOpenExternally = (url: string): boolean => {
|
|
try {
|
|
const parsed = new URL(url)
|
|
return SAFE_EXTERNAL_PROTOCOLS.has(parsed.protocol)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const loadSplash = async (windowRef: BrowserWindow) => {
|
|
if (process.env.ELECTRON_RENDERER_URL) {
|
|
await windowRef.loadURL(process.env.ELECTRON_RENDERER_URL)
|
|
return
|
|
}
|
|
|
|
await windowRef.loadFile(join(__dirname, '../renderer/index.html'))
|
|
}
|
|
|
|
const createWindow = async () => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.focus()
|
|
return
|
|
}
|
|
|
|
const windowRef = new BrowserWindow({
|
|
width: 1200,
|
|
height: 800,
|
|
show: false,
|
|
webPreferences: {
|
|
preload: join(__dirname, '../preload/index.js'),
|
|
sandbox: true,
|
|
},
|
|
})
|
|
mainWindow = windowRef
|
|
|
|
windowRef.webContents.setWindowOpenHandler(({ url }) => {
|
|
if (!canOpenExternally(url)) {
|
|
if (!app.isPackaged) {
|
|
console.warn(`Blocked external URL: ${url}`)
|
|
}
|
|
|
|
return { action: 'deny' }
|
|
}
|
|
|
|
void shell.openExternal(url)
|
|
return { action: 'deny' }
|
|
})
|
|
|
|
windowRef.on('closed', () => {
|
|
if (mainWindow === windowRef) {
|
|
mainWindow = null
|
|
}
|
|
})
|
|
|
|
try {
|
|
await loadSplash(windowRef)
|
|
} catch (error) {
|
|
if (mainWindow === windowRef) {
|
|
mainWindow = null
|
|
}
|
|
|
|
if (!windowRef.isDestroyed()) {
|
|
windowRef.destroy()
|
|
}
|
|
|
|
throw error
|
|
}
|
|
|
|
if (!windowRef.isDestroyed()) {
|
|
windowRef.show()
|
|
}
|
|
|
|
const targetUrl = await sidecar.resolveUrl()
|
|
if (isQuitting || windowRef.isDestroyed()) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await windowRef.loadURL(targetUrl)
|
|
} catch (error) {
|
|
if (mainWindow === windowRef) {
|
|
mainWindow = null
|
|
}
|
|
|
|
if (!windowRef.isDestroyed()) {
|
|
windowRef.destroy()
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const ensureWindow = async () => {
|
|
if (windowCreationPromise) {
|
|
return windowCreationPromise
|
|
}
|
|
|
|
windowCreationPromise = createWindow().finally(() => {
|
|
windowCreationPromise = null
|
|
})
|
|
|
|
return windowCreationPromise
|
|
}
|
|
|
|
const beginQuit = () => {
|
|
isQuitting = true
|
|
sidecar.stop()
|
|
}
|
|
|
|
const handleWindowCreationError = (error: unknown, context: string) => {
|
|
console.error(`${context}:`, error)
|
|
showErrorAndQuit(
|
|
"App Couldn't Start",
|
|
app.isPackaged
|
|
? 'A required component failed to start. Please reinstall the app.'
|
|
: `${context}: ${toErrorMessage(error)}`,
|
|
)
|
|
}
|
|
|
|
app
|
|
.whenReady()
|
|
.then(() => ensureWindow())
|
|
.catch((error) => {
|
|
handleWindowCreationError(error, 'Failed to create window')
|
|
})
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') {
|
|
app.quit()
|
|
}
|
|
})
|
|
|
|
app.on('activate', () => {
|
|
if (isQuitting || BrowserWindow.getAllWindows().length > 0) {
|
|
return
|
|
}
|
|
|
|
ensureWindow().catch((error) => {
|
|
handleWindowCreationError(error, 'Failed to re-create window')
|
|
})
|
|
})
|
|
|
|
app.on('before-quit', beginQuit)
|