mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-30 20:39:46 +02:00
electron build improvements
This commit is contained in:
parent
72897e279f
commit
9d2bd22a5a
6 changed files with 58 additions and 281 deletions
2
.github/workflows/electron-build.yml
vendored
2
.github/workflows/electron-build.yml
vendored
|
|
@ -94,6 +94,8 @@ jobs:
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }}
|
||||||
|
VITE_PUBLIC_POSTHOG_HOST: ${{ secrets.VITE_PUBLIC_POSTHOG_HOST }}
|
||||||
run: npm run publish
|
run: npm run publish
|
||||||
working-directory: apps/x/apps/main
|
working-directory: apps/x/apps/main
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
# Electron Main Process - Build & Packaging
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This is the Electron main process for the Rowboat app.
|
|
||||||
|
|
||||||
## Why We Use esbuild Bundling
|
|
||||||
|
|
||||||
**Problem**: pnpm uses symlinks for workspace packages (`@x/core`, `@x/shared`).
|
|
||||||
Electron Forge's dependency walker (`flora-colossus`) cannot follow these symlinks,
|
|
||||||
causing "Failed to locate module" errors during packaging. Note: npm workspaces
|
|
||||||
also use symlinks, so this isn't pnpm-specific.
|
|
||||||
|
|
||||||
**Solution**: Bundle the entire main process into a single JS file using esbuild.
|
|
||||||
This inlines all dependencies (except `electron` itself), eliminating the need
|
|
||||||
for `node_modules` at runtime.
|
|
||||||
|
|
||||||
## Bundle Configuration (`bundle.mjs`)
|
|
||||||
|
|
||||||
The bundler uses these key settings:
|
|
||||||
|
|
||||||
- **Format: CommonJS** - Many dependencies use `require()` which doesn't work
|
|
||||||
with esbuild's ESM shim. CJS handles dynamic requires natively.
|
|
||||||
|
|
||||||
- **import.meta.url polyfill** - The source code uses ESM's `import.meta.url`
|
|
||||||
to derive `__dirname`, but CJS doesn't have `import.meta`. We solve this with:
|
|
||||||
- `banner`: Injects `var __import_meta_url = require('url').pathToFileURL(__filename).href;`
|
|
||||||
- `define`: Replaces all `import.meta.url` with `__import_meta_url`
|
|
||||||
|
|
||||||
- **External: electron** - Not bundled; provided by Electron runtime.
|
|
||||||
|
|
||||||
## Build Process
|
|
||||||
|
|
||||||
The build uses two Forge hooks in `forge.config.cjs`:
|
|
||||||
|
|
||||||
### 1. `generateAssets` Hook (Pre-packaging)
|
|
||||||
Prepares all build artifacts in a hidden `.package/` staging directory:
|
|
||||||
- Builds shared, renderer, preload, and main TypeScript
|
|
||||||
- Bundles main process with esbuild → `.package/dist-bundle/main.js`
|
|
||||||
- Copies preload/renderer dist to `.package/`
|
|
||||||
|
|
||||||
### 2. `packageAfterCopy` Hook (Post-copy)
|
|
||||||
After Forge copies source to output, this hook replaces source files with bundled/staged files:
|
|
||||||
- **Hook signature**: `async (config, buildPath, electronVersion, platform, arch)`
|
|
||||||
- `buildPath` already points to `Contents/Resources/app` (not the `.app` bundle root)
|
|
||||||
- Removes unbundled `dist/` directory (has unresolvable `@x/core` imports)
|
|
||||||
- Copies bundled `dist-bundle/` from `.package/` staging directory
|
|
||||||
- Copies `preload/` and `renderer/` directories from staging
|
|
||||||
- Updates `package.json`: sets `main` to `dist-bundle/main.js`, removes
|
|
||||||
`"type": "module"` (since we bundle as CJS), removes all dependencies/devDependencies
|
|
||||||
- Cleans up source files (src/, tsconfig.json, forge.config.cjs, agents.md, .gitignore, bundle.mjs)
|
|
||||||
|
|
||||||
**Why this approach?** Electron Forge ignores `packagerConfig.dir` and always
|
|
||||||
packages from the config file's directory. The `packageAfterCopy` hook is the
|
|
||||||
reliable way to customize the packaged output by modifying files after Forge
|
|
||||||
copies the source directory but before the app bundle is finalized.
|
|
||||||
|
|
||||||
## Staged Build Directory (`.package/`)
|
|
||||||
|
|
||||||
- **Why not copy into apps/main directly?** Would pollute source with build artifacts
|
|
||||||
- **Why .cjs extension for forge.config?** package.json has `"type": "module"`,
|
|
||||||
but Forge loads configs with `require()`. The `.cjs` extension forces CommonJS.
|
|
||||||
- **Why hidden (`.` prefix)?** Prevents accidental conflicts with developer-created dirs
|
|
||||||
|
|
||||||
## Development vs Production Paths
|
|
||||||
|
|
||||||
| Mode | main.js location | preload path | renderer path |
|
|
||||||
|------|------------------|--------------|---------------|
|
|
||||||
| Dev | `dist/main.js` | `../../preload/dist/` | `../../renderer/dist/` |
|
|
||||||
| Prod | `dist-bundle/main.js` | `../preload/dist/` | `../renderer/dist/` |
|
|
||||||
|
|
||||||
Code uses `app.isPackaged` to select the correct paths at runtime.
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
- `npm run start` - Development (runs from dist/, uses Vite dev server)
|
|
||||||
- `npm run package` - Creates .app bundle in `out/`
|
|
||||||
- `npm run make` - Creates DMG/ZIP in `out/make/`
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
If the packaged app fails with module errors:
|
|
||||||
1. **Clean build**: `rm -rf out .package && npm run make`
|
|
||||||
2. **Reinstall fresh**: Delete `/Applications/Rowboat.app` before installing DMG
|
|
||||||
3. **Clear caches**: `rm -rf ~/Library/Caches/com.rowboat.app`
|
|
||||||
|
|
@ -21,12 +21,11 @@ await esbuild.build({
|
||||||
bundle: true,
|
bundle: true,
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
target: 'node20',
|
target: 'node20',
|
||||||
outfile: './.package/dist-bundle/main.js',
|
outfile: './.package/dist/main.cjs',
|
||||||
external: ['electron'], // Provided by Electron runtime
|
external: ['electron'], // Provided by Electron runtime
|
||||||
// Use CommonJS format - many dependencies use require() which doesn't work
|
// Use CommonJS format - many dependencies use require() which doesn't work
|
||||||
// well with esbuild's ESM shim. CJS handles dynamic requires natively.
|
// well with esbuild's ESM shim. CJS handles dynamic requires natively.
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true,
|
|
||||||
// Inject the polyfill variable at the top
|
// Inject the polyfill variable at the top
|
||||||
banner: { js: cjsBanner },
|
banner: { js: cjsBanner },
|
||||||
// Replace import.meta.url directly with our polyfill variable
|
// Replace import.meta.url directly with our polyfill variable
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,11 @@ module.exports = {
|
||||||
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.
|
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.
|
||||||
prune: false,
|
prune: false,
|
||||||
ignore: [
|
ignore: [
|
||||||
// Skip any node_modules that might exist
|
/src\//,
|
||||||
/node_modules/,
|
/node_modules\//,
|
||||||
// Skip source files
|
/.gitignore/,
|
||||||
/\.ts$/,
|
/bundle\.mjs/,
|
||||||
/\.tsx$/,
|
/tsconfig.json/,
|
||||||
// Skip the staging directory
|
|
||||||
/\.package/,
|
|
||||||
// Skip the bundle script
|
|
||||||
/bundle\.mjs$/,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
makers: [
|
makers: [
|
||||||
|
|
@ -142,111 +138,7 @@ module.exports = {
|
||||||
fs.mkdirSync(rendererDest, { recursive: true });
|
fs.mkdirSync(rendererDest, { recursive: true });
|
||||||
fs.cpSync(rendererSrc, rendererDest, { recursive: true });
|
fs.cpSync(rendererSrc, rendererDest, { recursive: true });
|
||||||
|
|
||||||
// Copy icons into staging directory
|
|
||||||
console.log('Copying icons...');
|
|
||||||
const iconsSrc = path.join(__dirname, 'icons');
|
|
||||||
const iconsDest = path.join(packageDir, 'icons');
|
|
||||||
if (fs.existsSync(iconsSrc)) {
|
|
||||||
fs.mkdirSync(iconsDest, { recursive: true });
|
|
||||||
fs.cpSync(iconsSrc, iconsDest, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate package.json in staging directory
|
|
||||||
// This tells Electron where to find the entry point
|
|
||||||
// Note: No "type": "module" since we bundle as CommonJS for compatibility
|
|
||||||
// with dependencies that use dynamic require()
|
|
||||||
// Read version from source package.json (updated by CI from git tag)
|
|
||||||
const sourcePackageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
||||||
const packageJson = {
|
|
||||||
name: 'Rowboat',
|
|
||||||
version: sourcePackageJson.version,
|
|
||||||
main: 'dist-bundle/main.js',
|
|
||||||
};
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(packageDir, 'package.json'),
|
|
||||||
JSON.stringify(packageJson, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ All assets staged in .package/');
|
console.log('✅ All assets staged in .package/');
|
||||||
},
|
},
|
||||||
// Hook signature: async (config, buildPath, electronVersion, platform, arch)
|
|
||||||
// Called after Forge copies source directory to build output
|
|
||||||
// This is where we replace source files with bundled/staged files
|
|
||||||
packageAfterCopy: async (config, buildPath, electronVersion, platform, arch) => {
|
|
||||||
const fs = require('fs');
|
|
||||||
const packageDir = path.join(__dirname, '.package');
|
|
||||||
// buildPath already points to the app directory (Contents/Resources/app)
|
|
||||||
const appResourcesPath = buildPath;
|
|
||||||
|
|
||||||
console.log('📦 Copying staged files from .package/ to packaged app...');
|
|
||||||
|
|
||||||
// Remove unbundled dist/ directory (source TypeScript output)
|
|
||||||
const unbundledDist = path.join(appResourcesPath, 'dist');
|
|
||||||
if (fs.existsSync(unbundledDist)) {
|
|
||||||
console.log('Removing unbundled dist/...');
|
|
||||||
fs.rmSync(unbundledDist, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy bundled dist-bundle/ from staging
|
|
||||||
const distBundleSrc = path.join(packageDir, 'dist-bundle');
|
|
||||||
const distBundleDest = path.join(appResourcesPath, 'dist-bundle');
|
|
||||||
if (fs.existsSync(distBundleSrc)) {
|
|
||||||
console.log('Copying dist-bundle/...');
|
|
||||||
fs.mkdirSync(distBundleDest, { recursive: true });
|
|
||||||
fs.cpSync(distBundleSrc, distBundleDest, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy preload/ from staging
|
|
||||||
const preloadSrc = path.join(packageDir, 'preload');
|
|
||||||
const preloadDest = path.join(appResourcesPath, 'preload');
|
|
||||||
if (fs.existsSync(preloadSrc)) {
|
|
||||||
console.log('Copying preload/...');
|
|
||||||
// Remove old preload if it exists
|
|
||||||
if (fs.existsSync(preloadDest)) {
|
|
||||||
fs.rmSync(preloadDest, { recursive: true });
|
|
||||||
}
|
|
||||||
fs.cpSync(preloadSrc, preloadDest, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy renderer/ from staging
|
|
||||||
const rendererSrc = path.join(packageDir, 'renderer');
|
|
||||||
const rendererDest = path.join(appResourcesPath, 'renderer');
|
|
||||||
if (fs.existsSync(rendererSrc)) {
|
|
||||||
console.log('Copying renderer/...');
|
|
||||||
// Remove old renderer if it exists
|
|
||||||
if (fs.existsSync(rendererDest)) {
|
|
||||||
fs.rmSync(rendererDest, { recursive: true });
|
|
||||||
}
|
|
||||||
fs.cpSync(rendererSrc, rendererDest, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update package.json to point to bundled entry point
|
|
||||||
const packageJsonPath = path.join(appResourcesPath, 'package.json');
|
|
||||||
if (fs.existsSync(packageJsonPath)) {
|
|
||||||
console.log('Updating package.json...');
|
|
||||||
// Read version from source package.json (updated by CI from git tag)
|
|
||||||
const sourcePackageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
||||||
const packageJson = {
|
|
||||||
name: 'Rowboat',
|
|
||||||
version: sourcePackageJson.version,
|
|
||||||
main: 'dist-bundle/main.js',
|
|
||||||
// Note: No "type": "module" since we bundle as CommonJS
|
|
||||||
// No dependencies/devDependencies since everything is bundled
|
|
||||||
};
|
|
||||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up source files that shouldn't be in packaged app
|
|
||||||
const filesToRemove = ['src', 'tsconfig.json', 'forge.config.cjs', 'agents.md', '.gitignore', 'bundle.mjs'];
|
|
||||||
for (const file of filesToRemove) {
|
|
||||||
const filePath = path.join(appResourcesPath, file);
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
console.log(`Removing ${file}...`);
|
|
||||||
fs.rmSync(filePath, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Staged files copied to packaged app');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
"name": "Rowboat",
|
"name": "Rowboat",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"main": "dist/main.js",
|
"main": ".package/dist/main.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"build": "rm -rf dist && tsc",
|
"build": "rm -rf dist && tsc && node bundle.mjs",
|
||||||
"package": "electron-forge package --arch=arm64,x64 --platform=darwin",
|
"package": "electron-forge package --arch=arm64,x64 --platform=darwin",
|
||||||
"make": "electron-forge make --arch=arm64,x64 --platform=darwin",
|
"make": "electron-forge make --arch=arm64,x64 --platform=darwin",
|
||||||
"publish": "electron-forge publish --arch=arm64,x64 --platform=darwin"
|
"publish": "electron-forge publish --arch=arm64,x64 --platform=darwin"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import path from "node:path";
|
||||||
import { setupIpcHandlers, startRunsWatcher, startWorkspaceWatcher, stopWorkspaceWatcher } from "./ipc.js";
|
import { setupIpcHandlers, startRunsWatcher, startWorkspaceWatcher, stopWorkspaceWatcher } from "./ipc.js";
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
import { existsSync } from "node:fs";
|
|
||||||
import { updateElectronApp, UpdateSourceType } from "update-electron-app";
|
import { updateElectronApp, UpdateSourceType } from "update-electron-app";
|
||||||
import { init as initGmailSync } from "@x/core/dist/knowledge/sync_gmail.js";
|
import { init as initGmailSync } from "@x/core/dist/knowledge/sync_gmail.js";
|
||||||
import { init as initCalendarSync } from "@x/core/dist/knowledge/sync_calendar.js";
|
import { init as initCalendarSync } from "@x/core/dist/knowledge/sync_calendar.js";
|
||||||
|
|
@ -15,58 +14,51 @@ import { init as initPreBuiltRunner } from "@x/core/dist/pre_built/runner.js";
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
// #region agent log
|
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:14',message:'__dirname resolved',data:{__dirname,__filename,isPackaged:app.isPackaged},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// Path resolution differs between development and production:
|
// Path resolution differs between development and production:
|
||||||
// - Development: main.js runs from dist/, preload is at ../../preload/dist/ (sibling dir)
|
|
||||||
// - Production: main.js runs from .package/dist-bundle/, preload is at ../preload/dist/ (copied into .package/)
|
|
||||||
const preloadPath = app.isPackaged
|
const preloadPath = app.isPackaged
|
||||||
? path.join(__dirname, "../preload/dist/preload.js") // Production
|
? path.join(__dirname, "../preload/dist/preload.js")
|
||||||
: path.join(__dirname, "../../preload/dist/preload.js"); // Development
|
: path.join(__dirname, "../../../preload/dist/preload.js");
|
||||||
console.log("preloadPath", preloadPath);
|
console.log("preloadPath", preloadPath);
|
||||||
|
|
||||||
// #region agent log
|
const rendererPath = app.isPackaged
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:22',message:'preloadPath computed',data:{preloadPath,exists:existsSync(preloadPath)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
|
? path.join(__dirname, "../renderer/dist") // Production
|
||||||
// #endregion
|
: path.join(__dirname, "../../../renderer/dist"); // Development
|
||||||
|
console.log("rendererPath", rendererPath);
|
||||||
|
|
||||||
// Register custom protocol for serving built renderer files in production
|
// Register custom protocol for serving built renderer files in production.
|
||||||
|
// This keeps SPA routes working when users deep link into the packaged app.
|
||||||
function registerAppProtocol() {
|
function registerAppProtocol() {
|
||||||
protocol.handle('app', (request) => {
|
protocol.handle("app", (request) => {
|
||||||
// #region agent log
|
const url = new URL(request.url);
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:26',message:'protocol handler called',data:{url:request.url},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// Remove 'app://' prefix and get the path
|
// url.pathname starts with "/"
|
||||||
let urlPath = request.url.slice('app://'.length);
|
let urlPath = url.pathname;
|
||||||
|
|
||||||
// Remove leading './' if present
|
// If it's "/" or a SPA route (no extension), serve index.html
|
||||||
if (urlPath.startsWith('./')) {
|
if (urlPath === "/" || !path.extname(urlPath)) {
|
||||||
urlPath = urlPath.slice(2);
|
urlPath = "/index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to index.html for root or SPA routes (no file extension)
|
const filePath = path.join(rendererPath, urlPath);
|
||||||
if (!urlPath || urlPath === '/' || !path.extname(urlPath)) {
|
|
||||||
urlPath = 'index.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve to the renderer dist directory
|
|
||||||
// - Development: main.js at dist/, renderer at ../../renderer/dist/ (sibling dir)
|
|
||||||
// - Production: main.js at .package/dist-bundle/, renderer at ../renderer/dist/ (copied into .package/)
|
|
||||||
const rendererDistPath = app.isPackaged
|
|
||||||
? path.join(__dirname, '../renderer/dist')
|
|
||||||
: path.join(__dirname, '../../renderer/dist');
|
|
||||||
const filePath = path.join(rendererDistPath, urlPath);
|
|
||||||
|
|
||||||
// #region agent log
|
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:46',message:'renderer path resolution',data:{rendererDistPath,filePath,urlPath,exists:existsSync(filePath),rendererDistExists:existsSync(rendererDistPath)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
return net.fetch(pathToFileURL(filePath).toString());
|
return net.fetch(pathToFileURL(filePath).toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{
|
||||||
|
scheme: "app",
|
||||||
|
privileges: {
|
||||||
|
standard: true,
|
||||||
|
secure: true,
|
||||||
|
supportFetchAPI: true,
|
||||||
|
corsEnabled: true,
|
||||||
|
allowServiceWorkers: true,
|
||||||
|
// optional but often helpful:
|
||||||
|
// stream: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
|
|
@ -83,64 +75,41 @@ function createWindow() {
|
||||||
// Open external links in system browser (not sandboxed Electron window)
|
// Open external links in system browser (not sandboxed Electron window)
|
||||||
// This handles window.open() and target="_blank" links
|
// This handles window.open() and target="_blank" links
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
// Open all URLs in system browser
|
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
return { action: 'deny' }; // Prevent Electron from opening a new window
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle navigation to external URLs (e.g., clicking a link without target="_blank")
|
// Handle navigation to external URLs (e.g., clicking a link without target="_blank")
|
||||||
win.webContents.on('will-navigate', (event, url) => {
|
win.webContents.on("will-navigate", (event, url) => {
|
||||||
// Allow internal navigation (app protocol or dev server)
|
const isInternal =
|
||||||
const isInternal = url.startsWith('app://') || url.startsWith('http://localhost:5173');
|
url.startsWith("app://") || url.startsWith("http://localhost:5173");
|
||||||
if (!isInternal) {
|
if (!isInternal) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// #region agent log
|
|
||||||
const loadURL = app.isPackaged ? 'app://./' : 'http://localhost:5173';
|
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:65',message:'createWindow called',data:{isPackaged:app.isPackaged,loadURL,preloadPath},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
// Production: load from custom protocol (serves built renderer files)
|
win.loadURL("app://-/index.html");
|
||||||
win.loadURL('app://./');
|
|
||||||
|
|
||||||
// #region agent log
|
|
||||||
win.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
|
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:69',message:'window load failed',data:{errorCode,errorDescription,validatedURL},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
||||||
});
|
|
||||||
win.webContents.on('did-finish-load', () => {
|
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:72',message:'window load finished',data:{url:win.webContents.getURL()},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
||||||
});
|
|
||||||
// #endregion
|
|
||||||
} else {
|
} else {
|
||||||
// Development: load from Vite dev server
|
win.loadURL("http://localhost:5173");
|
||||||
win.loadURL('http://localhost:5173');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// #region agent log
|
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:74',message:'app.whenReady triggered',data:{isPackaged:app.isPackaged},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// Register custom protocol before creating window (for production builds)
|
// Register custom protocol before creating window (for production builds)
|
||||||
registerAppProtocol();
|
if (app.isPackaged) {
|
||||||
|
registerAppProtocol();
|
||||||
// #region agent log
|
}
|
||||||
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:77',message:'protocol registered',data:{isPackaged:app.isPackaged},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// Initialize auto-updater (only in production)
|
// Initialize auto-updater (only in production)
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
updateElectronApp({
|
updateElectronApp({
|
||||||
updateSource: {
|
updateSource: {
|
||||||
type: UpdateSourceType.StaticStorage,
|
type: UpdateSourceType.StaticStorage,
|
||||||
baseUrl: `https://rowboat-desktop-app-releases.s3.amazonaws.com/releases/${process.platform}/${process.arch}`
|
baseUrl: `https://rowboat-desktop-app-releases.s3.amazonaws.com/releases/${process.platform}/${process.arch}`,
|
||||||
},
|
},
|
||||||
notifyUser: true // Shows native dialog when update is available
|
notifyUser: true, // Shows native dialog when update is available
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,7 +124,6 @@ app.whenReady().then(() => {
|
||||||
// Only starts once (guarded in startWorkspaceWatcher)
|
// Only starts once (guarded in startWorkspaceWatcher)
|
||||||
startWorkspaceWatcher();
|
startWorkspaceWatcher();
|
||||||
|
|
||||||
|
|
||||||
// start runs watcher
|
// start runs watcher
|
||||||
startRunsWatcher();
|
startRunsWatcher();
|
||||||
|
|
||||||
|
|
@ -177,7 +145,7 @@ app.whenReady().then(() => {
|
||||||
// start pre-built agent runner
|
// start pre-built agent runner
|
||||||
initPreBuiltRunner();
|
initPreBuiltRunner();
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue