An Electron application with React and TypeScript
$ bun install$ bun run dev# For macOS
$ bun run build:mac:open:signed
## Project Structure Overview
electron-app/ ├── out/ # Build output (Main, Preload, Renderer JS files) ├── resources/ # Static assets (e.g., icons) ├── src/ │ ├── main/ # Main process source (Node.js environment) │ │ └── index.ts │ ├── preload/ # Preload script source │ │ ├── index.ts │ │ └── index.d.ts # Type definitions for preload API │ ├── renderer/ # Renderer process source (React UI) │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── main.tsx # React entry point │ │ │ ├── env.d.ts # Renderer environment types │ │ │ └── components/ # UI components (example) │ │ └── index.html # HTML entry for renderer │ └── native-modules/ # Native Node.js addons │ └── native-windows/ # Example native module │ ├── macos/ # macOS specific native code │ │ ├── activeWindowObserver.h │ │ ├── activeWindowObserver.mm │ │ └── nativeWindows.mm │ ├── binding.gyp # node-gyp build configuration │ ├── index.ts # TypeScript wrapper for native module │ └── package.json # for the native module ├── .eslintrc.js (or .mjs, eslint.config.mjs) # ESLint configuration ├── .gitignore ├── package.json ├── tsconfig.json # Base TypeScript configuration (if present) ├── tsconfig.node.json # TypeScript config for main/preload ├── tsconfig.web.json # TypeScript config for renderer └── README.md
Important: When implementing macOS-style window hiding (to keep tracking alive), be aware that BrowserWindow and webContents have independent lifecycles.
// ❌ WRONG - Only checks if window exists
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('message', data) // Can crash with "Object has been destroyed"
}
// ✅ CORRECT - Checks both window AND renderer
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
mainWindow.webContents.send('message', data) // Safe
}Why this happens: When you hide a window (instead of destroying it), Electron may destroy the webContents (renderer process) for memory management while keeping the BrowserWindow container alive. Always check both states before IPC communication.
This pattern is used throughout our codebase in:
src/main/ipc.ts- All IPC handlerssrc/main/index.ts- Active window trackingsrc/main/auto-updater.ts- Update status messages
The primary method for creating local builds is through electron-builder, which ensures that all packaging, code signing, and entitlements are handled correctly and consistently.
To create a full, signed production-ready build on your local machine (including a .dmg installer) without publishing it, run:
bun run build:mac:electron-builderThis command is the recommended way to create a local build for testing and verification. It builds, signs, and packages the application, producing a .dmg file in the dist/ directory.
Full Installation Cleanup
If you need to reset your local environment, you can use the cleanup script. This is useful when debugging permissions or first-launch issues.
bun run clean:cronus-installationThis script will:
- Delete
Cronus.appfrom your/Applicationsfolder. - Reset macOS permissions (TCC) for Apple Events and Accessibility for the app.
The Electron app uses different environment variables for development and production builds. These are managed via .env files in the electron-app directory:
.env.development: Used when running the app locally withbun run dev..env.production: Used for packaged builds created withelectron-builder.
These files are loaded at runtime by the main process (src/main/index.ts). Create these files from the example below and populate them with the necessary service keys.
# Example environment variables
GOOGLE_CLIENT_ID="your-google-client-id"
CLIENT_URL="your-client-url"
...
Cronus delivers automatic updates from the public S3 bucket cronusnewupdates (region us-east-1). Every packaged copy of the app checks this bucket on startup and whenever the user clicks Settings → Check for Updates.
The release process involves two main steps: updating the version and running the release script.
-
Bump the version in electron-app/package.json (e.g.
"1.0.8" → "1.0.9"). -
Run the release script from the
electron-appdirectory:Important: The command below contains placeholder AWS credentials. Before running, you must replace
YOUR_AWS_ACCESS_KEY_IDandYOUR_AWS_SECRET_ACCESS_KEYwith valid credentials that have permission to publish to the S3 bucket.Copy and paste the entire command below into your terminal. It chains all the necessary steps to clean, prepare, and publish a new release for all architectures.
bun run clean:cronus-installation && \ export NODE_ENV=production && \ cp -r ../node_modules/electron-updater node_modules/ && \ cp -r ../node_modules/fs-extra node_modules/ && \ cp -r ../node_modules/jsonfile node_modules/ && \ cp -r ../node_modules/debug node_modules/ && \ set -a && source .env.production && set +a && \ AWS_REGION="us-east-1" \ S3_BUCKET_NAME="cronusnewupdates" \ AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY_ID" \ AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_ACCESS_KEY" \ AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE=1 \ bun run publish:with-links:all
This command handles the entire release process:
- Cleans any previous local installation.
- Sets up the production environment and dependencies.
- Builds the application.
- Packages the
.dmgand.zipfiles. - Publishes the new version and its
latest-mac.ymlfile to S3. - Updates the
Cronus-latest-[arch].dmgandCronus-latest-[arch].zipfiles in S3 to point to the new version.
If you just need to create a local build for testing without uploading, use bun run build:for-publish:arm64 or bun run build:for-publish:x64.
The release script automatically handles updating the permanent download links. You can find them at:
- Latest ARM64 DMG:
https://cronusnewupdates.s3.amazonaws.com/Cronus-latest-arm64.dmg - Latest ARM64 ZIP:
https://cronusnewupdates.s3.amazonaws.com/Cronus-latest-arm64.zip - Latest Intel DMG:
https://cronusnewupdates.s3.amazonaws.com/Cronus-latest-x64.dmg - Latest Intel ZIP:
https://cronusnewupdates.s3.amazonaws.com/Cronus-latest-x64.zip
The website can use these fixed URLs and never needs updating - they will automatically serve the newest build.
| Symptom | Likely fix |
|---|---|
AccessControlListNotSupported |
Enable ACLs on the bucket or add "acl": null to the publish block in package.json. |
| No toast after "Check for updates" | Ensure the installed build has a lower version than the one referenced in latest-mac.yml. |
| Still nothing happens | Open DevTools → Console, look for update-status events to see errors or states. |
Missing module errors during runtime:
If you get "Cannot find module" errors (like debug, electron-updater, etc.), running the consolidated release script above should fix the issue by copying the required dependencies from the workspace root. This happens because electron-builder doesn't always properly resolve workspace dependencies.