create-electron-foundation
Version:
An interactive CLI to bootstrap a modern, type-safe, and scalable Electron application.
204 lines (177 loc) • 7.68 kB
text/typescript
import { app, BrowserWindow, shell, ipcMain } from 'electron'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import fs from 'fs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// TESTING
import assert from 'node:assert'
// ENV
import dotenv from 'dotenv'
// LOGGING
import log from './logger/index'
const mainLogger = log.scope('main/index.ts')
// REGISTER: controllers ######################################################
// this is where the .handle() function pairings for the render process
// .invoke() function calls
import './api/controller'
// CONFIGURE: environment variables ###########################################
const isProd = app?.isPackaged
const envPath = isProd
? path.join(process.resourcesPath, '.env.production')
: `.env.development`
try {
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath, override: true })
}
} catch (err) {
mainLogger.error(`No environment file found at:`, envPath, err)
process.exit(1)
}
// APP_ROOT: The root directory of the application.
process.env.APP_ROOT = path.join(__dirname, '../..')
assert(!!process.env.APP_ROOT, 'APP_ROOT is not set')
// DIST: The directory containing the bundled front-end code for the renderer process.
process.env.DIST = path.join(process.env.APP_ROOT, 'dist')
assert(!!process.env.DIST, 'DIST is not set')
// VITE_PUBLIC: The directory for static public assets. In development, this points to 'public',
// and in production, it points to the 'dist' directory.
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, 'public')
: process.env.DIST
assert(!!process.env.VITE_PUBLIC, 'VITE_PUBLIC is not set')
// CONFIGURE: preload script ##################################################
// Defines the path to the preload script.
// The preload script runs in a privileged environment and can bridge the gap
// between the sandboxed renderer process and the Node.js environment of the main process.
const preloadPath = path.join(__dirname, '../preload/index.js')
// CONFIGURE: main window #####################################################
const webPreferences = {
preload: preloadPath,
// enables Node.js integration in the renderer process.
// This should be used with caution as it can pose security risks.
// > 👀 just keep it disabled.
nodeIntegration: false,
// context isolation creates a separate JavaScript context for the preload script.
// This is a security measure that helps prevent the preload script from leaking privileged APIs
// to the renderer process's untrusted web content. It is highly recommended to keep this true.
// > 👀 just keep it on
contextIsolation: true,
}
let win: BrowserWindow | null = null
async function createWindow() {
win = new BrowserWindow({
title: 'Main window',
icon: path.join(process.env.VITE_PUBLIC!, 'favicon.ico'),
webPreferences,
})
// CONFIGURE: main window dev tools #########################################
if (process.env.NODE_ENV !== 'production') {
win.webContents.openDevTools()
}
// CONFIGURE: web links #####################################################
// This handler ensures that external links are opened in the system's
// default web browser instead of a new Electron window.
win.webContents.setWindowOpenHandler(({ url }) => {
const protocol = new URL(url).protocol
if (protocol === 'http:' || protocol === 'https:') {
shell.openExternal(url)
}
return { action: 'deny' }
})
// CONFIGURE ERROR HANDLING #################################################
// Handles the event where the renderer process crashes.
win.webContents.on('render-process-gone', (event, details) => {
mainLogger.error(`Renderer process crashed:`, details)
})
// Handles the event where the window fails to load content.
win.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
mainLogger.error(`Failed to load:`, { errorCode, errorDescription })
})
// CONFIGURE: content loading ###############################################
// If a Vite development server URL is available (development mode), it loads that URL.
// Otherwise (production mode), it attempts to load the local index.html file from the 'dist' directory.
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL)
} else {
const indexPath = path.join(process.env.DIST!, 'index.html')
if (fs.existsSync(indexPath)) {
win.loadFile(indexPath)
} else {
mainLogger.error(
`Index file not found at: ${indexPath}. This is the expected location for the production build.`
)
const errorHtml = `
<html>
<body style="background: #f44336; color: white; font-family: sans-serif; padding: 20px; text-align: center;">
<h1>Application Error</h1>
<p>Could not load the application's main page (index.html).</p>
<p>The file was not found in the expected directory:</p>
<p><code>${process.env.DIST}</code></p>
<p>Please ensure the application has been built correctly and all necessary files are present.</p>
</body>
</html>
`
win.loadURL(
`data:text/html;charset=utf-8,${encodeURIComponent(errorHtml)}`
)
}
}
}
// CONFIGURE: app ready #######################################################
app.whenReady().then(async () => {
mainLogger.info('🎉🎉 App is ready')
try {
// Creates the main application window after the database is initialized.
await createWindow()
} catch (error) {
mainLogger.error('🚨🚨 Failed to initialize application:', error)
app.quit()
}
})
// CONFIGURE: app events #######################################################
// Handles the 'activate' event, which is typically triggered when the application's
// icon is clicked in the dock (macOS) and there are no windows open.
// It creates a new window if none exist.
app.on('activate', () => {
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
// If windows exist, focus on the first one
allWindows[0].focus()
} else {
// If no windows exist, create a new one.
createWindow()
}
})
// Handles the 'window-all-closed' event, which is triggered when all application windows are closed.
// It quits the application, except on macOS where applications typically stay active
// even without open windows.
app.on('window-all-closed', () => {
win = null
if (process.platform !== 'darwin') app.quit()
})
// Handles the 'second-instance' event, which is triggered when a user tries to open
// a second instance of the application while one is already running.
// It focuses the existing main window instead of creating a new one.
app.on('second-instance', () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore()
win.focus()
}
})
// CONFIGURE: SINGLE / ORPHAN IPC HANDLERS ####################################
// Sets up an IPC (Inter-Process Communication) handler for the 'open-win' channel.
// This allows the renderer process to request the main process to open a new window.
// The 'arg' parameter can be used to pass data (e.g., a URL or route) to the new window.
ipcMain.handle('open-win', (_, arg) => {
const childWindow = new BrowserWindow({
webPreferences,
})
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#${arg}`)
} else {
childWindow.loadFile(path.join(process.env.DIST, 'index.html'), {
hash: arg,
})
}
})