├── template ├── .nvmrc ├── index.d.ts ├── .github │ ├── FUNDING.yml │ └── workflows │ │ └── release.yml ├── trusted-dependencies-scripts.json ├── .npmrc ├── src │ ├── resources │ │ ├── build │ │ │ └── icons │ │ │ │ ├── icon.icns │ │ │ │ └── icon.ico │ │ └── public │ │ │ └── illustration.svg │ ├── renderer │ │ ├── lib │ │ │ └── utils.ts │ │ ├── routes.tsx │ │ ├── index.tsx │ │ ├── index.html │ │ ├── screens │ │ │ └── main.tsx │ │ ├── components │ │ │ └── ui │ │ │ │ └── alert.tsx │ │ └── globals.css │ ├── lib │ │ ├── electron-app │ │ │ ├── factories │ │ │ │ ├── app │ │ │ │ │ ├── instance.ts │ │ │ │ │ └── setup.ts │ │ │ │ ├── windows │ │ │ │ │ └── create.ts │ │ │ │ └── ipcs │ │ │ │ │ └── register-window-creation.ts │ │ │ ├── release │ │ │ │ ├── utils │ │ │ │ │ ├── extractors.ts │ │ │ │ │ ├── path.ts │ │ │ │ │ ├── question.ts │ │ │ │ │ ├── exec.ts │ │ │ │ │ └── validations.ts │ │ │ │ ├── constants │ │ │ │ │ └── colors.ts │ │ │ │ └── modules │ │ │ │ │ ├── prebuild.ts │ │ │ │ │ └── release.ts │ │ │ └── utils │ │ │ │ └── ignore-console-warnings.ts │ │ └── electron-router-dom.ts │ ├── shared │ │ ├── constants.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── preload │ │ └── index.ts │ └── main │ │ ├── index.ts │ │ └── windows │ │ └── main.ts ├── .vscode │ ├── extensions.json │ └── settings.json ├── .editorconfig ├── .gitignore ├── components.json ├── tsconfig.json ├── electron-builder.ts ├── biome.json ├── electron.vite.config.ts └── package.json ├── docs ├── images │ ├── preview.png │ └── bullet.svg ├── STRUCTURE.md ├── RELEASING.md ├── SOURCE_CODE_PROTECTION.md ├── FAQ.md └── UNSIGNED_APPS.md ├── .gitignore ├── LICENSE └── README.md /template/.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /template/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /template/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: daltonmenezes 2 | patreon: daltonmenezes 3 | 4 | -------------------------------------------------------------------------------- /template/trusted-dependencies-scripts.json: -------------------------------------------------------------------------------- 1 | ["electron", "electron-vite", "@biomejs/biome"] 2 | -------------------------------------------------------------------------------- /docs/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranesh239/electron-app/main/docs/images/preview.png -------------------------------------------------------------------------------- /template/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false 4 | -------------------------------------------------------------------------------- /template/src/resources/build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranesh239/electron-app/main/template/src/resources/build/icons/icon.icns -------------------------------------------------------------------------------- /template/src/resources/build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pranesh239/electron-app/main/template/src/resources/build/icons/icon.ico -------------------------------------------------------------------------------- /template/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome", "bradlc.vscode-tailwindcss"], 3 | "unwantedRecommendations": [ 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /template/src/renderer/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /template/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /template/src/lib/electron-app/factories/app/instance.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | 3 | export function makeAppWithSingleInstanceLock(fn: () => void) { 4 | const isPrimaryInstance = app.requestSingleInstanceLock() 5 | 6 | !isPrimaryInstance ? app.quit() : fn() 7 | } 8 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/release/utils/extractors.ts: -------------------------------------------------------------------------------- 1 | export function extractOwnerAndRepoFromGitRemoteURL(url: string) { 2 | return url 3 | ?.replace(/^git@github.com:|.git$/gims, '') 4 | ?.replace(/^https:\/\/github.com\/|.git$/gims, '') 5 | ?.trim() 6 | } 7 | -------------------------------------------------------------------------------- /template/src/lib/electron-router-dom.ts: -------------------------------------------------------------------------------- 1 | import { createElectronRouter } from 'electron-router-dom' 2 | 3 | export const { Router, registerRoute, settings } = createElectronRouter({ 4 | port: 4927, 5 | 6 | types: { 7 | ids: ['main', 'about'], 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/release/utils/path.ts: -------------------------------------------------------------------------------- 1 | import { normalize, dirname } from 'node:path' 2 | 3 | export function getDevFolder(path: string) { 4 | const [nodeModules, devFolder] = normalize(dirname(path)).split(/\/|\\/g) 5 | 6 | return [nodeModules, devFolder].join('/') 7 | } 8 | -------------------------------------------------------------------------------- /template/src/renderer/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from 'react-router-dom' 2 | 3 | import { Router } from 'lib/electron-router-dom' 4 | 5 | import { MainScreen } from './screens/main' 6 | 7 | export function AppRoutes() { 8 | return } />} /> 9 | } 10 | -------------------------------------------------------------------------------- /template/src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const ENVIRONMENT = { 2 | IS_DEV: process.env.NODE_ENV === 'development', 3 | } 4 | 5 | export const PLATFORM = { 6 | IS_MAC: process.platform === 'darwin', 7 | IS_WINDOWS: process.platform === 'win32', 8 | IS_LINUX: process.platform === 'linux', 9 | } 10 | -------------------------------------------------------------------------------- /template/src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDom from 'react-dom/client' 2 | import React from 'react' 3 | 4 | import { AppRoutes } from './routes' 5 | 6 | import './globals.css' 7 | 8 | ReactDom.createRoot(document.querySelector('app') as HTMLElement).render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /template/src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron' 2 | 3 | declare global { 4 | interface Window { 5 | App: typeof API 6 | } 7 | } 8 | 9 | const API = { 10 | sayHelloFromBridge: () => console.log('\nHello from bridgeAPI! 👋\n\n'), 11 | username: process.env.USER, 12 | } 13 | 14 | contextBridge.exposeInMainWorld('App', API) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/release/constants/colors.ts: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | RED: '\x1b[31m', 3 | RESET: '\x1b[0m', 4 | GRAY: '\x1b[90m', 5 | BLUE: '\x1b[34m', 6 | CYAN: '\x1b[36m', 7 | GREEN: '\x1b[32m', 8 | WHITE: '\x1b[37m', 9 | YELLOW: '\x1b[33m', 10 | MAGENTA: '\x1b[35m', 11 | LIGHT_GRAY: '\x1b[37m', 12 | SOFT_GRAY: '\x1b[38;5;244m', 13 | } 14 | -------------------------------------------------------------------------------- /template/src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | 3 | import { makeAppWithSingleInstanceLock } from 'lib/electron-app/factories/app/instance' 4 | import { makeAppSetup } from 'lib/electron-app/factories/app/setup' 5 | import { MainWindow } from './windows/main' 6 | 7 | makeAppWithSingleInstanceLock(async () => { 8 | await app.whenReady() 9 | await makeAppSetup(MainWindow) 10 | }) 11 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/release/utils/question.ts: -------------------------------------------------------------------------------- 1 | import Readline from 'node:readline' 2 | 3 | export function question(question: string): Promise { 4 | const readline = Readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout, 7 | }) 8 | 9 | return new Promise(resolve => { 10 | readline.question(question, answer => { 11 | readline.close() 12 | resolve(answer) 13 | }) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/utils/ignore-console-warnings.ts: -------------------------------------------------------------------------------- 1 | export function ignoreConsoleWarnings(warningsToIgnore: string[]) { 2 | const originalEmitWarning = process.emitWarning 3 | 4 | process.emitWarning = (warning, ...args) => { 5 | if ( 6 | typeof warning === 'string' && 7 | warningsToIgnore.length > 0 && 8 | warningsToIgnore.some(ignoredWarning => warning.includes(ignoredWarning)) 9 | ) { 10 | return 11 | } 12 | 13 | originalEmitWarning(warning, ...(args as string[])) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /template/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/renderer/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "renderer/components", 15 | "utils": "renderer/utils", 16 | "ui": "renderer/components/ui", 17 | "lib": "renderer/lib", 18 | "hooks": "renderer/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/factories/windows/create.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import { join } from 'node:path' 3 | 4 | import type { WindowProps } from 'shared/types' 5 | 6 | import { registerRoute } from 'lib/electron-router-dom' 7 | 8 | export function createWindow({ id, ...settings }: WindowProps) { 9 | const window = new BrowserWindow(settings) 10 | 11 | registerRoute({ 12 | id, 13 | browserWindow: window, 14 | htmlFile: join(__dirname, '../renderer/index.html'), 15 | }) 16 | 17 | window.on('closed', window.destroy) 18 | 19 | return window 20 | } 21 | -------------------------------------------------------------------------------- /template/src/shared/types.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserWindow, IpcMainInvokeEvent } from 'electron' 2 | 3 | import type { registerRoute } from 'lib/electron-router-dom' 4 | 5 | export type BrowserWindowOrNull = Electron.BrowserWindow | null 6 | 7 | type Route = Parameters[0] 8 | 9 | export interface WindowProps extends Electron.BrowserWindowConstructorOptions { 10 | id: Route['id'] 11 | query?: Route['query'] 12 | } 13 | 14 | export interface WindowCreationByIPC { 15 | channel: string 16 | window(): BrowserWindowOrNull 17 | callback(window: BrowserWindow, event: IpcMainInvokeEvent): void 18 | } 19 | -------------------------------------------------------------------------------- /template/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.experimental.configFile": "src/renderer/globals.css", 3 | "files.associations": { 4 | "*.css": "tailwindcss" 5 | }, 6 | "editor.quickSuggestions": { 7 | "strings": "on" 8 | }, 9 | 10 | "editor.defaultFormatter": "biomejs.biome", 11 | "biome.enabled": true, 12 | 13 | "[javascript]": { 14 | "editor.formatOnSave": true 15 | }, 16 | 17 | "[typescript]": { 18 | "editor.formatOnSave": true 19 | }, 20 | 21 | "[typescriptreact]": { 22 | "editor.formatOnSave": true 23 | }, 24 | 25 | "[json]": { 26 | "editor.formatOnSave": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/factories/ipcs/register-window-creation.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | 3 | import type { WindowCreationByIPC, BrowserWindowOrNull } from 'shared/types' 4 | 5 | export function registerWindowCreationByIPC({ 6 | channel, 7 | callback, 8 | window: createWindow, 9 | }: WindowCreationByIPC) { 10 | let window: BrowserWindowOrNull 11 | 12 | ipcMain.handle(channel, event => { 13 | if (!createWindow || window) return 14 | 15 | window = createWindow() as NonNullable 16 | 17 | window.on('closed', () => { 18 | window = null 19 | }) 20 | 21 | callback?.(window, event) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /template/src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import { author as _author, name } from '~/package.json' 2 | 3 | const author = _author.name ?? _author 4 | const authorInKebabCase = author.replace(/\s+/g, '-') 5 | const appId = `com.${authorInKebabCase}.${name}`.toLowerCase() 6 | 7 | /** 8 | * @param {string} id 9 | * @description Create the app id using the name and author from package.json transformed to kebab case if the id is not provided. 10 | * @default 'com.{author}.{app}' - the author and app comes from package.json 11 | * @example 12 | * makeAppId('com.example.app') 13 | * // => 'com.example.app' 14 | */ 15 | export function makeAppId(id: string = appId): string { 16 | return id 17 | } 18 | -------------------------------------------------------------------------------- /template/src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/release/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | execSync, 3 | type ExecSyncOptionsWithStringEncoding, 4 | } from 'node:child_process' 5 | 6 | import { resolve } from 'node:path' 7 | 8 | interface Options { 9 | inherit?: boolean 10 | } 11 | 12 | function makeOptions( 13 | options?: Options 14 | ): Partial { 15 | return { 16 | stdio: options?.inherit ? 'inherit' : 'pipe', 17 | cwd: resolve(), 18 | encoding: 'utf8', 19 | } 20 | } 21 | 22 | export function exec(commands: string[], options?: Options) { 23 | const outputs = [] 24 | 25 | for (const command of commands) { 26 | const output = execSync(command, makeOptions(options)) as string 27 | outputs.push(output) 28 | } 29 | 30 | return outputs 31 | } 32 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "lib": ["esnext", "dom", "dom.iterable"], 6 | "jsx": "react-jsx", 7 | "importHelpers": true, 8 | "moduleResolution": "bundler", 9 | "module": "ESNext", 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "sourceMap": true, 13 | "isolatedModules": true, 14 | "allowJs": true, 15 | "allowSyntheticDefaultImports": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noEmit": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "*": ["src/*"], 22 | "~/*": ["./*"] 23 | } 24 | }, 25 | "include": [ 26 | "./src", 27 | "electron.vite.config.*", 28 | "electron-builder.ts", 29 | "index.d.ts" 30 | ], 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /docs/STRUCTURE.md: -------------------------------------------------------------------------------- 1 |

Structure Overview

2 | 3 |
4 | 5 | ## src/lib 6 | 7 | A folder containing lib configurations/instances. 8 | 9 | ## src/main 10 | 11 | A folder containing the main process files and folders. 12 | 13 | ## src/renderer 14 | 15 | A folder containing the renderer process files and folders. ReactJS lives here! 16 | 17 | ## src/preload 18 | A folder containing the preload script that expose the API connection between main and renderer world by IPC in the context bridge. 19 | 20 | ## src/resources 21 | 22 | A folder containing public assets and assets for the build process like icons. 23 | 24 | > **Note**: all the content inside the **public** folder will be copied to the builded version as its. 25 | 26 | ## src/shared 27 | 28 | A folder containing data shared between one or more processes, such as constants, utilities, types, etc. 29 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/release/utils/validations.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver' 2 | 3 | import { COLORS } from '../constants/colors' 4 | 5 | export function checkValidations({ 6 | version, 7 | newVersion, 8 | }: { version: string; newVersion: string }) { 9 | if (!newVersion) { 10 | console.log(`${COLORS.RED}No version entered${COLORS.RESET}`) 11 | 12 | return true 13 | } 14 | 15 | if (!semver.valid(newVersion)) { 16 | console.log( 17 | `${COLORS.RED}Version must have a semver format (${COLORS.SOFT_GRAY}x.x.x${COLORS.RESET} example: ${COLORS.GREEN}1.0.1${COLORS.RESET}${COLORS.RED})${COLORS.RESET}` 18 | ) 19 | 20 | return true 21 | } 22 | 23 | if (semver.ltr(newVersion, version)) { 24 | console.log( 25 | `${COLORS.RED}New version is lower than current version${COLORS.RESET}` 26 | ) 27 | 28 | return true 29 | } 30 | 31 | if (semver.eq(newVersion, version)) { 32 | console.log( 33 | `${COLORS.RED}New version is equal to current version${COLORS.RESET}` 34 | ) 35 | 36 | return true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /template/src/main/windows/main.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import { join } from 'node:path' 3 | 4 | import { createWindow } from 'lib/electron-app/factories/windows/create' 5 | import { ENVIRONMENT } from 'shared/constants' 6 | import { displayName } from '~/package.json' 7 | 8 | export async function MainWindow() { 9 | const window = createWindow({ 10 | id: 'main', 11 | title: displayName, 12 | width: 700, 13 | height: 473, 14 | show: false, 15 | center: true, 16 | movable: true, 17 | resizable: false, 18 | alwaysOnTop: true, 19 | autoHideMenuBar: true, 20 | 21 | webPreferences: { 22 | preload: join(__dirname, '../preload/index.js'), 23 | }, 24 | }) 25 | 26 | window.webContents.on('did-finish-load', () => { 27 | if (ENVIRONMENT.IS_DEV) { 28 | window.webContents.openDevTools({ mode: 'detach' }) 29 | } 30 | 31 | window.show() 32 | }) 33 | 34 | window.on('close', () => { 35 | for (const window of BrowserWindow.getAllWindows()) { 36 | window.destroy() 37 | } 38 | }) 39 | 40 | return window 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 - * Dalton Menezes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /template/src/renderer/screens/main.tsx: -------------------------------------------------------------------------------- 1 | import { Terminal } from 'lucide-react' 2 | import { useEffect } from 'react' 3 | 4 | import { 5 | Alert, 6 | AlertTitle, 7 | AlertDescription, 8 | } from 'renderer/components/ui/alert' 9 | 10 | // The "App" comes from the context bridge in preload/index.ts 11 | const { App } = window 12 | 13 | export function MainScreen() { 14 | useEffect(() => { 15 | // check the console on dev tools 16 | App.sayHelloFromBridge() 17 | }, []) 18 | 19 | const userName = App.username || 'there' 20 | 21 | return ( 22 |
23 | 24 | 25 | Hi, {userName}! 26 | 27 | 28 | 29 | 30 | 31 | 32 | It's time to build something awesome! 33 | 34 | 35 | 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/release/modules/prebuild.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises' 2 | import { resolve } from 'node:path' 3 | 4 | import trustedDependencies from '../../../../../trusted-dependencies-scripts.json' 5 | import packageJSON from '../../../../../package.json' 6 | import { getDevFolder } from '../utils/path' 7 | 8 | async function createPackageJSONDistVersion() { 9 | const { main, scripts, resources, devDependencies, ...rest } = packageJSON 10 | 11 | const packageJSONDistVersion = { 12 | main: './main/index.js', 13 | ...rest, 14 | } 15 | 16 | try { 17 | await Promise.all([ 18 | writeFile( 19 | resolve(getDevFolder(main), 'package.json'), 20 | JSON.stringify(packageJSONDistVersion, null, 2) 21 | ), 22 | 23 | writeFile( 24 | resolve(getDevFolder(main), packageJSON.pnpm.onlyBuiltDependenciesFile), 25 | JSON.stringify(trustedDependencies, null, 2) 26 | ), 27 | ]) 28 | } catch ({ message }: any) { 29 | console.log(` 30 | 🛑 Something went wrong!\n 31 | 🧐 There was a problem creating the package.json dist version...\n 32 | 👀 Error: ${message} 33 | `) 34 | } 35 | } 36 | 37 | createPackageJSONDistVersion() 38 | -------------------------------------------------------------------------------- /docs/RELEASING.md: -------------------------------------------------------------------------------- 1 |

Releasing

2 | 3 | > **Note**: to be able to perform `auto-updates` you will need a `code signed app`, for this purpose you will need to configure it by yourself, so check the [electron-builder](https://www.electron.build/code-signing) and [action-electron-builder](https://github.com/daltonmenezes/action-electron-builder#code-signing) docs please to get know how to do this. 4 | 5 | To release your app on a GitHub release with `Windows`, `Mac` and `Linux` binaries, you can perform the following commands: 6 | 7 | ```bash 8 | git pull 9 | pnpm make:release 10 | ``` 11 | 12 | Then, enter the new version of your app, so it will produce the following binaries in a `draft release` from the action: 13 | - `Windows` → `zip (portable)`, `.exe` 14 | - `Mac` → `.zip (app)`, `.dmg` 15 | - `Linux` → `AppImage`, `freebsd`, `pacman`, `rpm`, `deb` 16 | 17 | In this process, the action will be triggered and the previous command will open the `releases` and `actions` page in your browser. When the action is finished, you can click in the `releases` page and refresh it to see the draft release with the binaries, so you can edit it and release it for your users. 18 | 19 | https://user-images.githubusercontent.com/1149845/156939675-5ea0c510-ddd3-4de7-b293-87d3697bd1a8.mp4 20 | 21 | -------------------------------------------------------------------------------- /template/electron-builder.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from 'electron-builder' 2 | 3 | import { 4 | main, 5 | name, 6 | version, 7 | resources, 8 | description, 9 | displayName, 10 | author as _author, 11 | } from './package.json' 12 | 13 | import { getDevFolder } from './src/lib/electron-app/release/utils/path' 14 | 15 | const author = _author?.name ?? _author 16 | const currentYear = new Date().getFullYear() 17 | const authorInKebabCase = author.replace(/\s+/g, '-') 18 | const appId = `com.${authorInKebabCase}.${name}`.toLowerCase() 19 | 20 | const artifactName = [`${name}-v${version}`, '-${os}.${ext}'].join('') 21 | 22 | export default { 23 | appId, 24 | productName: displayName, 25 | copyright: `Copyright © ${currentYear} — ${author}`, 26 | 27 | directories: { 28 | app: getDevFolder(main), 29 | output: `dist/v${version}`, 30 | }, 31 | 32 | mac: { 33 | artifactName, 34 | icon: `${resources}/build/icons/icon.icns`, 35 | category: 'public.app-category.utilities', 36 | target: ['zip', 'dmg', 'dir'], 37 | }, 38 | 39 | linux: { 40 | artifactName, 41 | category: 'Utilities', 42 | synopsis: description, 43 | target: ['AppImage', 'deb', 'pacman', 'freebsd', 'rpm'], 44 | }, 45 | 46 | win: { 47 | artifactName, 48 | icon: `${resources}/build/icons/icon.ico`, 49 | target: ['zip', 'portable'], 50 | }, 51 | } satisfies Configuration 52 | -------------------------------------------------------------------------------- /template/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "formatter": { 7 | "enabled": true, 8 | "indentStyle": "space", 9 | "indentWidth": 2, 10 | "lineWidth": 80, 11 | "lineEnding": "lf" 12 | }, 13 | "javascript": { 14 | "formatter": { 15 | "arrowParentheses": "asNeeded", 16 | "jsxQuoteStyle": "double", 17 | "quoteStyle": "single", 18 | "semicolons": "asNeeded", 19 | "trailingCommas": "es5" 20 | } 21 | }, 22 | "linter": { 23 | "enabled": true, 24 | "rules": { 25 | "recommended": true, 26 | "a11y": { 27 | "noSvgWithoutTitle": "off", 28 | "useButtonType": "off" 29 | }, 30 | "suspicious": { 31 | "noArrayIndexKey": "info", 32 | "noDuplicateObjectKeys": "warn", 33 | "noExplicitAny": "off", 34 | "noEmptyInterface": "off" 35 | }, 36 | "style": { 37 | "noUselessElse": "warn" 38 | }, 39 | "nursery": { 40 | "noDuplicateProperties": "warn" 41 | }, 42 | "correctness": { 43 | "noUnusedImports": "warn", 44 | "useExhaustiveDependencies": "off" 45 | }, 46 | "complexity": { 47 | "noBannedTypes": "warn" 48 | } 49 | } 50 | }, 51 | "files": { 52 | "ignore": ["dist", "node_modules"] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/SOURCE_CODE_PROTECTION.md: -------------------------------------------------------------------------------- 1 |

Source Code Protection

2 | 3 | > This process is done via [v8 bytecode compilation](https://nodejs.org/api/vm.html#vm_script_createcacheddata), to get more knowledge about it, please, [check the Electron Vite docs](https://evite.netlify.app/guide/source-code-protection.html). 4 | 5 | Use the `bytecodePlugin` from `electron-vite` to enable it in the **electron.vite.config.ts**: 6 | 7 | ```ts 8 | import { defineConfig, bytecodePlugin } from 'electron-vite' 9 | 10 | export default defineConfig({ 11 | main: { 12 | plugins: [tsconfigPaths, bytecodePlugin()] 13 | }, 14 | 15 | preload: { 16 | // Note: you will get the following warning using bytecodePlugin in the preload script in production build: "The vm module of Node.js is deprecated in the renderer process and will be removed", is up to you to keep bytecodePlugin here. Also, keep following the Electron Vite docs for more updates about this plugin! 17 | plugins: [tsconfigPaths, bytecodePlugin()] 18 | }, 19 | 20 | renderer: { 21 | // ... 22 | } 23 | }) 24 | ``` 25 | Also, `sandbox` should be `false` in `webPreferences` for the windows you are using a preload script like: 26 | ```ts 27 | const window = createWindow({ 28 | id: 'main', 29 | 30 | webPreferences: { 31 | preload: join(__dirname, '../preload/index.js'), 32 | sandbox: false, 33 | }, 34 | }) 35 | ``` -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 |

FAQ - Frequently Asked Questions

2 | 3 | ## How can I fix the `electron-builder install-app-deps Windows Script Host` error? 4 | If you are trying to use this boilerplate with npm instead pnpm on Windows, you will get that error related to `electron-builder install-app-deps`, so: 5 | - Take a look at [this comment](https://github.com/ficonsulting/RInno/issues/44#issuecomment-992299431) to fix it! 6 | 7 | ## What should I do if the release action fails and the re-run failed jobs keep failing? 8 | Go to the releases page in your repository and delete the draft release, then execute the action again but in the `re-run all jobs` mode. 9 | 10 | ## What are Autofill errors in the terminal? 11 | If you see the following errors in the terminal, you can ignore them: 12 | ```bash 13 | [97794:0301/202707.879855:ERROR:CONSOLE(1)] "Request Autofill.enable failed. {"code":-32601,"message":"'Autofill.enable' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1) 14 | [97794:0301/202707.879884:ERROR:CONSOLE(1)] "Request Autofill.setAddresses failed. {"code":-32601,"message":"'Autofill.setAddresses' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1) 15 | ``` 16 | It only happens when devtools panel is open and it's not an issue with your app. 17 | 18 | For more information, take a look at [this issue](https://github.com/electron/electron/issues/41614). 19 | 20 | -------------------------------------------------------------------------------- /template/.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: ["*"] 4 | 5 | jobs: 6 | release: 7 | if: startsWith(github.ref, 'refs/tags/v') 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [macos-latest, ubuntu-latest, windows-latest] 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - run: npm i -g --force corepack && corepack enable 18 | 19 | - name: Install Node.js, NPM and PNPM 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | cache: "pnpm" 24 | - run: pnpm install 25 | 26 | - name: apt-update 27 | if: startsWith(matrix.os, 'ubuntu-latest') 28 | run: sudo apt-get update 29 | 30 | - name: autoremove 31 | if: startsWith(matrix.os, 'ubuntu-latest') 32 | run: sudo apt autoremove 33 | 34 | - name: Install libarchive rpm on Linux 35 | if: startsWith(matrix.os, 'ubuntu-latest') 36 | run: sudo apt-get install libarchive-tools rpm 37 | 38 | - name: Release Electron app 39 | uses: daltonmenezes/action-electron-builder@v1.0.1 40 | with: 41 | package_manager: "pnpm" 42 | # GitHub token, automatically provided to the action 43 | # (No need to define this secret in the repo settings) 44 | github_token: ${{ secrets.github_token }} 45 | 46 | # If the commit is tagged with a version (e.g. "v1.0.0"), 47 | # release the app after building 48 | release: true 49 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/factories/app/setup.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | 3 | import { 4 | installExtension, 5 | REACT_DEVELOPER_TOOLS, 6 | } from 'electron-extension-installer' 7 | 8 | import { ignoreConsoleWarnings } from '../../utils/ignore-console-warnings' 9 | import { PLATFORM, ENVIRONMENT } from 'shared/constants' 10 | import { makeAppId } from 'shared/utils' 11 | 12 | ignoreConsoleWarnings(['Manifest version 2 is deprecated']) 13 | 14 | export async function makeAppSetup(createWindow: () => Promise) { 15 | if (ENVIRONMENT.IS_DEV) { 16 | await installExtension([REACT_DEVELOPER_TOOLS], { 17 | loadExtensionOptions: { 18 | allowFileAccess: true, 19 | }, 20 | }) 21 | } 22 | 23 | let window = await createWindow() 24 | 25 | app.on('activate', async () => { 26 | const windows = BrowserWindow.getAllWindows() 27 | 28 | if (!windows.length) { 29 | window = await createWindow() 30 | } else { 31 | for (window of windows.reverse()) { 32 | window.restore() 33 | } 34 | } 35 | }) 36 | 37 | app.on('web-contents-created', (_, contents) => 38 | contents.on( 39 | 'will-navigate', 40 | (event, _) => !ENVIRONMENT.IS_DEV && event.preventDefault() 41 | ) 42 | ) 43 | 44 | app.on('window-all-closed', () => !PLATFORM.IS_MAC && app.quit()) 45 | 46 | return window 47 | } 48 | 49 | PLATFORM.IS_LINUX && app.disableHardwareAcceleration() 50 | 51 | PLATFORM.IS_WINDOWS && 52 | app.setAppUserModelId(ENVIRONMENT.IS_DEV ? process.execPath : makeAppId()) 53 | 54 | app.commandLine.appendSwitch('force-color-profile', 'srgb') 55 | -------------------------------------------------------------------------------- /docs/images/bullet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/UNSIGNED_APPS.md: -------------------------------------------------------------------------------- 1 |

Running released unsigend apps

2 | 3 | ## Introduction 4 | 5 | This document explains how to run your distributed unsigned apps, which is a common scenario when you are developing and can't afford to sign your apps yet. 6 | 7 | ## macOS 8 | By default, macOS blocks the execution of unsigned apps. To bypass this: 9 | 10 | 1. **Download the app** and move it to the `Applications` folder (optional). 11 | 2. **When trying to open it, you will see a warning that the app cannot be opened.** 12 | 3. **Go to** `System Preferences` > `Security & Privacy` > `General`. 13 | 4. **In the "Allow apps downloaded from" section**, click `Open Anyway` for the blocked app. 14 | 5. If needed, run this command in the Terminal: 15 | ```sh 16 | xattr -c '/Applications/Your App.app' 17 | ``` 18 | or 19 | ```sh 20 | xattr -d com.apple.quarantine '/Applications/Your App.app' 21 | ``` 22 | or 23 | ```sh 24 | codesign --force --deep --sign - '/Applications/Your App.app' 25 | ``` 26 | 6. The app should now open normally. 27 | 28 | ## Linux 29 | On Linux, execution permission may be disabled by default. To run the app: 30 | 31 | 1. **Download the app** and move it to the desired folder. 32 | 2. **Open the Terminal** and navigate to the app's folder. 33 | 3. **Run the following command**: 34 | ```sh 35 | chmod +x YourApp 36 | ``` 37 | 4. **Now you can run the app** by double-clicking it or running it from the Terminal. 38 | 39 | ## Windows 40 | On Windows, SmartScreen may block the app. To run it: 41 | 42 | 1. **When trying to open the app, a Windows Defender SmartScreen warning will appear.** 43 | 2. Click `More info`. 44 | 3. Click `Run anyway`. 45 | 46 | Now you can run your Electron app without issues! 47 | 48 | -------------------------------------------------------------------------------- /template/src/renderer/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from 'react' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | 4 | import { cn } from 'renderer/lib/utils' 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-background text-foreground', 12 | destructive: 13 | 'text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | } 20 | ) 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<'div'> & VariantProps) { 27 | return ( 28 |
34 | ) 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { 38 | return ( 39 |
47 | ) 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<'div'>) { 54 | return ( 55 |
63 | ) 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription } 67 | -------------------------------------------------------------------------------- /template/src/lib/electron-app/release/modules/release.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises' 2 | import { resolve } from 'node:path' 3 | import open from 'open' 4 | 5 | import { extractOwnerAndRepoFromGitRemoteURL } from '../utils/extractors' 6 | import packageJSON from '../../../../../package.json' 7 | import { checkValidations } from '../utils/validations' 8 | import { question } from '../utils/question' 9 | import { COLORS } from '../constants/colors' 10 | import { exec } from '../utils/exec' 11 | 12 | async function makeRelease() { 13 | console.clear() 14 | 15 | const { version } = packageJSON 16 | 17 | const newVersion = await question( 18 | `Enter a new version: ${COLORS.SOFT_GRAY}(current is ${version})${COLORS.RESET} ` 19 | ) 20 | 21 | if (checkValidations({ version, newVersion })) { 22 | return 23 | } 24 | 25 | packageJSON.version = newVersion 26 | 27 | try { 28 | console.log( 29 | `${COLORS.CYAN}> Updating package.json version...${COLORS.RESET}` 30 | ) 31 | 32 | await writeFile( 33 | resolve('package.json'), 34 | JSON.stringify(packageJSON, null, 2) 35 | ) 36 | 37 | console.log(`\n${COLORS.GREEN}Done!${COLORS.RESET}\n`) 38 | console.log(`${COLORS.CYAN}> Trying to release it...${COLORS.RESET}`) 39 | 40 | exec( 41 | [ 42 | `git commit -am v${newVersion}`, 43 | `git tag v${newVersion}`, 44 | 'git push', 45 | 'git push --tags', 46 | ], 47 | { 48 | inherit: true, 49 | } 50 | ) 51 | 52 | const [repository] = exec(['git remote get-url --push origin']) 53 | const ownerAndRepo = extractOwnerAndRepoFromGitRemoteURL(repository) 54 | 55 | console.log( 56 | `${COLORS.CYAN}> Opening the repository releases page...${COLORS.RESET}` 57 | ) 58 | 59 | await open(`https://github.com/${ownerAndRepo}/releases`) 60 | 61 | console.log( 62 | `${COLORS.CYAN}> Opening the repository actions page...${COLORS.RESET}` 63 | ) 64 | 65 | await open(`https://github.com/${ownerAndRepo}/actions`) 66 | 67 | console.log(`\n${COLORS.GREEN}Done!${COLORS.RESET}\n`) 68 | } catch ({ message }: any) { 69 | console.log(` 70 | 🛑 Something went wrong!\n 71 | 👀 Error: ${message} 72 | `) 73 | } 74 | } 75 | 76 | makeRelease() 77 | -------------------------------------------------------------------------------- /template/electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 2 | import { codeInspectorPlugin } from 'code-inspector-plugin' 3 | import { resolve, normalize, dirname } from 'node:path' 4 | import tailwindcss from '@tailwindcss/vite' 5 | 6 | import injectProcessEnvPlugin from 'rollup-plugin-inject-process-env' 7 | import tsconfigPathsPlugin from 'vite-tsconfig-paths' 8 | import reactPlugin from '@vitejs/plugin-react' 9 | 10 | import { settings } from './src/lib/electron-router-dom' 11 | import { main, resources } from './package.json' 12 | 13 | const [nodeModules, devFolder] = normalize(dirname(main)).split(/\/|\\/g) 14 | const devPath = [nodeModules, devFolder].join('/') 15 | 16 | const tsconfigPaths = tsconfigPathsPlugin({ 17 | projects: [resolve('tsconfig.json')], 18 | }) 19 | 20 | export default defineConfig({ 21 | main: { 22 | plugins: [tsconfigPaths, externalizeDepsPlugin()], 23 | 24 | build: { 25 | rollupOptions: { 26 | input: { 27 | index: resolve('src/main/index.ts'), 28 | }, 29 | 30 | output: { 31 | dir: resolve(devPath, 'main'), 32 | }, 33 | }, 34 | }, 35 | }, 36 | 37 | preload: { 38 | plugins: [tsconfigPaths, externalizeDepsPlugin()], 39 | 40 | build: { 41 | outDir: resolve(devPath, 'preload'), 42 | }, 43 | }, 44 | 45 | renderer: { 46 | define: { 47 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 48 | 'process.platform': JSON.stringify(process.platform), 49 | }, 50 | 51 | server: { 52 | port: settings.port, 53 | }, 54 | 55 | plugins: [ 56 | tsconfigPaths, 57 | tailwindcss(), 58 | reactPlugin(), 59 | 60 | codeInspectorPlugin({ 61 | bundler: 'vite', 62 | hotKeys: ['altKey'], 63 | hideConsole: true, 64 | }), 65 | ], 66 | 67 | publicDir: resolve(resources, 'public'), 68 | 69 | build: { 70 | outDir: resolve(devPath, 'renderer'), 71 | 72 | rollupOptions: { 73 | plugins: [ 74 | injectProcessEnvPlugin({ 75 | NODE_ENV: 'production', 76 | platform: process.platform, 77 | }), 78 | ], 79 | 80 | input: { 81 | index: resolve('src/renderer/index.html'), 82 | }, 83 | 84 | output: { 85 | dir: resolve(devPath, 'renderer'), 86 | }, 87 | }, 88 | }, 89 | }, 90 | }) 91 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "My Electron App", 3 | "name": "my-electron-app", 4 | "description": "Your awesome app description", 5 | "version": "0.0.0", 6 | "main": "./node_modules/.dev/main/index.js", 7 | "resources": "src/resources", 8 | "author": { 9 | "name": "Dalton Menezes", 10 | "email": "daltonmenezes@outlook.com" 11 | }, 12 | "license": "MIT", 13 | "packageManager": "pnpm@10.0.0", 14 | "pnpm": { 15 | "onlyBuiltDependenciesFile": "./trusted-dependencies-scripts.json" 16 | }, 17 | "scripts": { 18 | "start": "electron-vite preview", 19 | "predev": "run-s clean:dev", 20 | "dev": "cross-env NODE_ENV=development electron-vite dev --watch", 21 | "compile:app": "electron-vite build", 22 | "compile:packageJSON": "tsx ./src/lib/electron-app/release/modules/prebuild.ts", 23 | "prebuild": "run-s clean:dev compile:app compile:packageJSON", 24 | "build": "pnpm electron-builder", 25 | "postinstall": "run-s prebuild install:deps", 26 | "install:deps": "electron-builder install-app-deps", 27 | "make:release": "tsx ./src/lib/electron-app/release/modules/release.ts", 28 | "release": "electron-builder --publish always", 29 | "clean:dev": "rimraf ./node_modules/.dev", 30 | "lint": "biome lint --no-errors-on-unmatched", 31 | "lint:fix": "biome lint --write --no-errors-on-unmatched" 32 | }, 33 | "dependencies": { 34 | "class-variance-authority": "^0.7.1", 35 | "clsx": "^2.1.1", 36 | "electron-router-dom": "^2.1.0", 37 | "lucide-react": "^0.477.0", 38 | "react": "^19.0.0", 39 | "react-dom": "^19.0.0", 40 | "react-router-dom": "^7.2.0", 41 | "tailwind-merge": "^3.0.2", 42 | "tailwindcss-animate": "^1.0.7" 43 | }, 44 | "devDependencies": { 45 | "@biomejs/biome": "^1.9.4", 46 | "@tailwindcss/vite": "^4.0.9", 47 | "@types/node": "^22.13.8", 48 | "@types/react": "^19.0.10", 49 | "@types/react-dom": "^19.0.4", 50 | "@types/semver": "^7.5.8", 51 | "@vitejs/plugin-react": "^4.0.4", 52 | "code-inspector-plugin": "^0.20.1", 53 | "cross-env": "^7.0.3", 54 | "electron": "^34.3.0", 55 | "electron-builder": "^25.1.8", 56 | "electron-extension-installer": "^1.2.0", 57 | "electron-vite": "^3.0.0", 58 | "npm-run-all": "^4.1.5", 59 | "open": "^10.1.0", 60 | "rimraf": "^6.0.1", 61 | "rollup-plugin-inject-process-env": "^1.3.1", 62 | "semver": "^7.5.4", 63 | "tailwindcss": "^4.0.9", 64 | "tsx": "^4.19.3", 65 | "typescript": "^5.1.6", 66 | "vite": "^6.2.0", 67 | "vite-tsconfig-paths": "^5.1.4" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /template/src/renderer/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin 'tailwindcss-animate'; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | :root { 8 | --background: oklch(1 0 0); 9 | --foreground: oklch(0.145 0 0); 10 | --card: oklch(1 0 0); 11 | --card-foreground: oklch(0.145 0 0); 12 | --popover: oklch(1 0 0); 13 | --popover-foreground: oklch(0.145 0 0); 14 | --primary: oklch(0.205 0 0); 15 | --primary-foreground: oklch(0.985 0 0); 16 | --secondary: oklch(0.97 0 0); 17 | --secondary-foreground: oklch(0.205 0 0); 18 | --muted: oklch(0.97 0 0); 19 | --muted-foreground: oklch(0.556 0 0); 20 | --accent: oklch(0.97 0 0); 21 | --accent-foreground: oklch(0.205 0 0); 22 | --destructive: oklch(0.577 0.245 27.325); 23 | --destructive-foreground: oklch(0.577 0.245 27.325); 24 | --border: oklch(0.922 0 0); 25 | --input: oklch(0.922 0 0); 26 | --ring: oklch(0.708 0 0); 27 | --chart-1: oklch(0.646 0.222 41.116); 28 | --chart-2: oklch(0.6 0.118 184.704); 29 | --chart-3: oklch(0.398 0.07 227.392); 30 | --chart-4: oklch(0.828 0.189 84.429); 31 | --chart-5: oklch(0.769 0.188 70.08); 32 | --radius: 0.625rem; 33 | --sidebar: oklch(0.985 0 0); 34 | --sidebar-foreground: oklch(0.145 0 0); 35 | --sidebar-primary: oklch(0.205 0 0); 36 | --sidebar-primary-foreground: oklch(0.985 0 0); 37 | --sidebar-accent: oklch(0.97 0 0); 38 | --sidebar-accent-foreground: oklch(0.205 0 0); 39 | --sidebar-border: oklch(0.922 0 0); 40 | --sidebar-ring: oklch(0.708 0 0); 41 | } 42 | 43 | .dark { 44 | --background: oklch(0.145 0 0); 45 | --foreground: oklch(0.985 0 0); 46 | --card: oklch(0.145 0 0); 47 | --card-foreground: oklch(0.985 0 0); 48 | --popover: oklch(0.145 0 0); 49 | --popover-foreground: oklch(0.985 0 0); 50 | --primary: oklch(0.985 0 0); 51 | --primary-foreground: oklch(0.205 0 0); 52 | --secondary: oklch(0.269 0 0); 53 | --secondary-foreground: oklch(0.985 0 0); 54 | --muted: oklch(0.269 0 0); 55 | --muted-foreground: oklch(0.708 0 0); 56 | --accent: oklch(0.269 0 0); 57 | --accent-foreground: oklch(0.985 0 0); 58 | --destructive: oklch(0.396 0.141 25.723); 59 | --destructive-foreground: oklch(0.637 0.237 25.331); 60 | --border: oklch(0.269 0 0); 61 | --input: oklch(0.269 0 0); 62 | --ring: oklch(0.439 0 0); 63 | --chart-1: oklch(0.488 0.243 264.376); 64 | --chart-2: oklch(0.696 0.17 162.48); 65 | --chart-3: oklch(0.769 0.188 70.08); 66 | --chart-4: oklch(0.627 0.265 303.9); 67 | --chart-5: oklch(0.645 0.246 16.439); 68 | --sidebar: oklch(0.205 0 0); 69 | --sidebar-foreground: oklch(0.985 0 0); 70 | --sidebar-primary: oklch(0.488 0.243 264.376); 71 | --sidebar-primary-foreground: oklch(0.985 0 0); 72 | --sidebar-accent: oklch(0.269 0 0); 73 | --sidebar-accent-foreground: oklch(0.985 0 0); 74 | --sidebar-border: oklch(0.269 0 0); 75 | --sidebar-ring: oklch(0.439 0 0); 76 | } 77 | 78 | @theme inline { 79 | --color-background: var(--background); 80 | --color-foreground: var(--foreground); 81 | --color-card: var(--card); 82 | --color-card-foreground: var(--card-foreground); 83 | --color-popover: var(--popover); 84 | --color-popover-foreground: var(--popover-foreground); 85 | --color-primary: var(--primary); 86 | --color-primary-foreground: var(--primary-foreground); 87 | --color-secondary: var(--secondary); 88 | --color-secondary-foreground: var(--secondary-foreground); 89 | --color-muted: var(--muted); 90 | --color-muted-foreground: var(--muted-foreground); 91 | --color-accent: var(--accent); 92 | --color-accent-foreground: var(--accent-foreground); 93 | --color-destructive: var(--destructive); 94 | --color-destructive-foreground: var(--destructive-foreground); 95 | --color-border: var(--border); 96 | --color-input: var(--input); 97 | --color-ring: var(--ring); 98 | --color-chart-1: var(--chart-1); 99 | --color-chart-2: var(--chart-2); 100 | --color-chart-3: var(--chart-3); 101 | --color-chart-4: var(--chart-4); 102 | --color-chart-5: var(--chart-5); 103 | --radius-sm: calc(var(--radius) - 4px); 104 | --radius-md: calc(var(--radius) - 2px); 105 | --radius-lg: var(--radius); 106 | --radius-xl: calc(var(--radius) + 4px); 107 | --color-sidebar: var(--sidebar); 108 | --color-sidebar-foreground: var(--sidebar-foreground); 109 | --color-sidebar-primary: var(--sidebar-primary); 110 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 111 | --color-sidebar-accent: var(--sidebar-accent); 112 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 113 | --color-sidebar-border: var(--sidebar-border); 114 | --color-sidebar-ring: var(--sidebar-ring); 115 | } 116 | 117 | @layer base { 118 | * { 119 | @apply border-border outline-ring/50; 120 | @apply antialiased; 121 | } 122 | 123 | body { 124 | @apply bg-background text-foreground; 125 | } 126 | 127 | html, 128 | body { 129 | user-select: none; 130 | scroll-behavior: smooth; 131 | -webkit-app-region: no-drag; 132 | -webkit-font-smoothing: antialiased; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Electron App

2 | 3 |

💅 An Electron app boilerplate with React v19, TypeScript v5, Tailwind v4, shadcn/ui, Electron Vite, Biome, GitHub Action releases and more. 4 |

5 | 6 | 7 | github url 8 | 9 | 10 | 11 | patreon url 12 | 13 | 14 | 15 | releases url 16 | 17 | 18 | 19 | license url 20 | 21 |

22 | 23 |

24 | 25 | preview 26 | 27 |

28 | 29 | # Features 30 | - **Stands out** 31 | - 🔥 Fast and Ready-to-go with a well-thought-out structure 32 | - 🚀 Auto reload for main and **Fast Refresh** for renderer process 33 | - 🎉 Window/Screen routing included 34 | - 😎 Preload (context bridge) already configured 35 | - 🔮 GitHub Action releases with `Windows`, `Mac` and `Linux` binaries 36 | - 🔒 Source Code Protection support 37 | - 🍪 Absolute paths support 38 | - **Technologies**: 39 | - 🔋 Electron 40 | - 🔥 ReactJS v19 41 | - 🌎 React Router DOM v7 and Electron Router DOM v2 42 | - 🧐 React Developer Tools 43 | - 🔍 Code inspector (holding `Alt` or `Option` key on DOM element and clicking on it) 44 | - 💙 TypeScript v5 45 | - 📦 Electron Vite 46 | - ✨ TailwindCSS v4 47 | - 🎨 shadcn/ui 48 | - 🍦 lucide-icons 49 | - 💫 Biome / EditorConfig 50 | - 📦 Electron Builder 51 | - 🔮 action-electron-builder 52 | 53 |
54 | 55 | > :warning: If **Windows 7** and **8** support is important for your project, you should know that Electron in a version greater than 22x no longer supports it. You can read more about it [here](https://www.electronjs.org/docs/latest/breaking-changes#removed-windows-7--8--81-support). Therefore, you must downgrade Electron to 22x version if it's important for you! 56 | 57 | # Requirements 58 | - [Node.js 20](https://nodejs.org/en/download/) 59 | - [pnpm 10](https://pnpm.io/installation) 60 | 61 | # Installation 62 | ```bash 63 | npx degit daltonmenezes/electron-app/template project_name 64 | ``` 65 | ```bash 66 | cd project_name 67 | pnpm install 68 | pnpm dev 69 | ``` 70 | 71 | Now, look at the **package.json** file in the root directory, you should update some of that settings with your project branding. 72 | 73 | # Adding new dependencies 74 | For security reasons, **pnpm** has the [onlyBuiltDependenciesFile](https://pnpm.io/package_json#pnpmonlybuiltdependenciesfile) property where only 75 | dependencies listed in the [trusted-dependencies-scripts.json](./template/trusted-dependencies-scripts.json) file can perform the postscripts execution. So, if you want to add a new dependency that needs to run a postscript, you should add it to the [trusted-dependencies-scripts.json](./template/trusted-dependencies-scripts.json) file list. 76 | 77 | # Distribution 78 | 79 | ### For all platforms 80 | 81 | > **Note**: Check [Electron Builder docs](https://www.electron.build/cli) for more knowledge 82 | 83 | ``` 84 | pnpm build 85 | ``` 86 | 87 | ### For a specific one 88 | 89 | ```bash 90 | pnpm build --mac 91 | # OR 92 | pnpm build --win 93 | # OR 94 | pnpm build --linux 95 | ``` 96 | 97 | The builded apps will be available in the `dist` folder. 98 | 99 | # Documents 100 | 101 | 102 | 107 | 112 | 117 | 118 | 119 | 124 | 129 | 134 | 135 |
103 |

104 | Routing 105 |

106 |
108 |

109 | Structure Overview 110 |

111 |
113 |

114 | Source Code Protection 115 |

116 |
120 |

121 | Releasing 122 |

123 |
125 |

126 | Running released unsigend apps 127 |

128 |
130 |

131 | FAQ - Frequently Asked Questions 132 |

133 |
136 | 137 | # Contributing 138 | > **Note**: contributions are always welcome, but always **ask first**, — please — before work on a PR. 139 | 140 | That said, there's a bunch of ways you can contribute to this project, like by: 141 | 142 | - :beetle: Reporting a bug 143 | - :page_facing_up: Improving this documentation 144 | - :rotating_light: Sharing this project and recommending it to your friends 145 | - :dollar: Supporting this project on GitHub Sponsors or Patreon 146 | - :star2: Giving a star on this repository 147 | 148 | # License 149 | 150 | [MIT © Dalton Menezes](https://github.com/daltonmenezes/electron-app/blob/main/LICENSE) 151 | -------------------------------------------------------------------------------- /template/src/resources/public/illustration.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | --------------------------------------------------------------------------------