Ensures renderer console.log output (including layout-debug) is printed to stdout in packaged builds, not just the log file. Also removes unnecessary webContents event listeners.
301 lines
9.0 KiB
TypeScript
301 lines
9.0 KiB
TypeScript
import log from "electron-log/main";
|
|
log.transports.console.level = "info";
|
|
log.initialize({ spyRendererConsole: true });
|
|
|
|
import { inheritLoginShellEnv } from "./login-shell-env.js";
|
|
inheritLoginShellEnv();
|
|
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { existsSync } from "node:fs";
|
|
import { app, BrowserWindow, ipcMain, nativeImage, net, protocol } from "electron";
|
|
import { registerDaemonManager } from "./daemon/daemon-manager.js";
|
|
import {
|
|
parseCliPassthroughArgsFromArgv,
|
|
runCliPassthroughCommand,
|
|
} from "./daemon/runtime-paths.js";
|
|
import { closeAllTransportSessions } from "./daemon/local-transport.js";
|
|
import {
|
|
registerWindowManager,
|
|
getMainWindowChromeOptions,
|
|
getWindowBackgroundColor,
|
|
resolveSystemWindowTheme,
|
|
setupWindowResizeEvents,
|
|
setupDefaultContextMenu,
|
|
setupDragDropPrevention,
|
|
} from "./window/window-manager.js";
|
|
import { registerDialogHandlers } from "./features/dialogs.js";
|
|
import {
|
|
registerNotificationHandlers,
|
|
ensureNotificationCenterRegistration,
|
|
} from "./features/notifications.js";
|
|
import { registerOpenerHandlers } from "./features/opener.js";
|
|
import { setupApplicationMenu } from "./features/menu.js";
|
|
import { parseOpenProjectPathFromArgv } from "./open-project-routing.js";
|
|
|
|
const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
|
const APP_SCHEME = "paseo";
|
|
const OPEN_PROJECT_EVENT = "paseo:event:open-project";
|
|
app.setName("Paseo");
|
|
|
|
// Allow users to pass Chromium flags via PASEO_ELECTRON_FLAGS for debugging
|
|
// rendering issues (e.g. "--disable-gpu --ozone-platform=x11").
|
|
// Must run before app.whenReady().
|
|
const electronFlags = process.env.PASEO_ELECTRON_FLAGS?.trim();
|
|
if (electronFlags) {
|
|
for (const token of electronFlags.split(/\s+/)) {
|
|
const [key, ...rest] = token.replace(/^--/, "").split("=");
|
|
app.commandLine.appendSwitch(key, rest.join("=") || undefined);
|
|
}
|
|
log.info("[electron-flags]", electronFlags);
|
|
}
|
|
|
|
let pendingOpenProjectPath = parseOpenProjectPathFromArgv({
|
|
argv: process.argv,
|
|
isDefaultApp: process.defaultApp,
|
|
});
|
|
|
|
log.info("[open-project] argv:", process.argv);
|
|
log.info("[open-project] isDefaultApp:", process.defaultApp);
|
|
log.info("[open-project] pendingOpenProjectPath:", pendingOpenProjectPath);
|
|
|
|
// The renderer pulls the pending path on mount via IPC — this avoids
|
|
// a race where the push event arrives before React registers its listener.
|
|
ipcMain.handle("paseo:get-pending-open-project", () => {
|
|
log.info("[open-project] renderer requested pending path:", pendingOpenProjectPath);
|
|
const result = pendingOpenProjectPath;
|
|
pendingOpenProjectPath = null;
|
|
return result;
|
|
});
|
|
|
|
protocol.registerSchemesAsPrivileged([
|
|
{ scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Window creation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function getPreloadPath(): string {
|
|
return path.join(__dirname, "preload.js");
|
|
}
|
|
|
|
function getAppDistDir(): string {
|
|
if (app.isPackaged) {
|
|
return path.join(process.resourcesPath, "app-dist");
|
|
}
|
|
|
|
return path.resolve(__dirname, "../../app/dist");
|
|
}
|
|
|
|
function getWindowIconPath(): string | null {
|
|
const candidates = app.isPackaged
|
|
? process.platform === "win32"
|
|
? [path.join(process.resourcesPath, "icon.ico"), path.join(process.resourcesPath, "icon.png")]
|
|
: [path.join(process.resourcesPath, "icon.png")]
|
|
: process.platform === "darwin"
|
|
? [path.resolve(__dirname, "../assets/icon.png")]
|
|
: process.platform === "win32"
|
|
? [
|
|
path.resolve(__dirname, "../assets/icon.ico"),
|
|
path.resolve(__dirname, "../assets/icon.png"),
|
|
]
|
|
: [path.resolve(__dirname, "../assets/icon.png")];
|
|
|
|
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
}
|
|
|
|
function applyAppIcon(): void {
|
|
if (process.platform !== "darwin") {
|
|
return;
|
|
}
|
|
|
|
const iconPath = path.resolve(__dirname, "../assets/icon.png");
|
|
if (!existsSync(iconPath)) {
|
|
return;
|
|
}
|
|
|
|
const icon = nativeImage.createFromPath(iconPath);
|
|
if (icon.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
app.dock?.setIcon(icon);
|
|
}
|
|
|
|
async function createMainWindow(): Promise<void> {
|
|
const iconPath = getWindowIconPath();
|
|
const systemTheme = resolveSystemWindowTheme();
|
|
|
|
const mainWindow = new BrowserWindow({
|
|
width: 1200,
|
|
height: 800,
|
|
show: false,
|
|
backgroundColor: getWindowBackgroundColor(systemTheme),
|
|
...(iconPath ? { icon: iconPath } : {}),
|
|
...getMainWindowChromeOptions({
|
|
platform: process.platform,
|
|
theme: systemTheme,
|
|
}),
|
|
webPreferences: {
|
|
preload: getPreloadPath(),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
});
|
|
|
|
setupWindowResizeEvents(mainWindow);
|
|
setupDefaultContextMenu(mainWindow);
|
|
setupDragDropPrevention(mainWindow);
|
|
|
|
mainWindow.once("ready-to-show", () => {
|
|
mainWindow.show();
|
|
});
|
|
|
|
if (!app.isPackaged) {
|
|
const { loadReactDevTools } = await import("./features/react-devtools.js");
|
|
await loadReactDevTools();
|
|
await mainWindow.loadURL(DEV_SERVER_URL);
|
|
mainWindow.webContents.openDevTools({ mode: "detach" });
|
|
return;
|
|
}
|
|
|
|
await mainWindow.loadURL(`${APP_SCHEME}://app/`);
|
|
}
|
|
|
|
function sendOpenProjectEvent(win: BrowserWindow, projectPath: string): void {
|
|
const send = () => {
|
|
log.info("[open-project] sending event to renderer:", projectPath);
|
|
win.webContents.send(OPEN_PROJECT_EVENT, { path: projectPath });
|
|
};
|
|
|
|
if (win.webContents.isLoadingMainFrame()) {
|
|
log.info("[open-project] waiting for did-finish-load before sending event");
|
|
win.webContents.once("did-finish-load", send);
|
|
return;
|
|
}
|
|
|
|
send();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// App lifecycle
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function setupSingleInstanceLock(): boolean {
|
|
const gotLock = app.requestSingleInstanceLock();
|
|
if (!gotLock) {
|
|
app.quit();
|
|
return false;
|
|
}
|
|
|
|
app.on("second-instance", (_event, commandLine) => {
|
|
log.info("[open-project] second-instance commandLine:", commandLine);
|
|
const openProjectPath = parseOpenProjectPathFromArgv({
|
|
argv: commandLine,
|
|
isDefaultApp: false,
|
|
});
|
|
log.info("[open-project] second-instance openProjectPath:", openProjectPath);
|
|
const win = BrowserWindow.getAllWindows()[0];
|
|
if (win) {
|
|
win.show();
|
|
if (win.isMinimized()) win.restore();
|
|
win.focus();
|
|
if (openProjectPath) {
|
|
sendOpenProjectEvent(win, openProjectPath);
|
|
}
|
|
}
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
async function runCliPassthroughIfRequested(): Promise<boolean> {
|
|
const cliArgs = parseCliPassthroughArgsFromArgv(process.argv);
|
|
if (!cliArgs) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const exitCode = runCliPassthroughCommand(cliArgs);
|
|
process.exit(exitCode);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
process.stderr.write(`${message}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async function bootstrap(): Promise<void> {
|
|
if (!pendingOpenProjectPath && (await runCliPassthroughIfRequested())) {
|
|
return;
|
|
}
|
|
|
|
if (!setupSingleInstanceLock()) {
|
|
return;
|
|
}
|
|
|
|
await app.whenReady();
|
|
|
|
const appDistDir = getAppDistDir();
|
|
protocol.handle(APP_SCHEME, (request) => {
|
|
const { pathname, search, hash } = new URL(request.url);
|
|
const decodedPath = decodeURIComponent(pathname);
|
|
|
|
// Chromium can occasionally request the exported entrypoint directly.
|
|
// Canonicalize it back to the route URL so Expo Router sees `/`, not `/index.html`.
|
|
if (decodedPath.endsWith("/index.html")) {
|
|
const normalizedPath = decodedPath.slice(0, -"/index.html".length) || "/";
|
|
return Response.redirect(`${APP_SCHEME}://app${normalizedPath}${search}${hash}`, 307);
|
|
}
|
|
|
|
const filePath = path.join(appDistDir, decodedPath);
|
|
const relativePath = path.relative(appDistDir, filePath);
|
|
|
|
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
return new Response("Not found", { status: 404 });
|
|
}
|
|
|
|
// SPA fallback: serve index.html for routes without a file extension
|
|
if (!relativePath || !path.extname(relativePath)) {
|
|
return net.fetch(pathToFileURL(path.join(appDistDir, "index.html")).toString());
|
|
}
|
|
|
|
return net.fetch(pathToFileURL(filePath).toString());
|
|
});
|
|
|
|
applyAppIcon();
|
|
setupApplicationMenu();
|
|
ensureNotificationCenterRegistration();
|
|
registerDaemonManager();
|
|
registerWindowManager();
|
|
registerDialogHandlers();
|
|
registerNotificationHandlers();
|
|
registerOpenerHandlers();
|
|
await createMainWindow();
|
|
|
|
app.on("activate", async () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
await createMainWindow();
|
|
}
|
|
});
|
|
}
|
|
|
|
void bootstrap().catch((error) => {
|
|
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
process.stderr.write(`${message}\n`);
|
|
process.exit(1);
|
|
});
|
|
|
|
app.on("before-quit", () => {
|
|
closeAllTransportSessions();
|
|
});
|
|
|
|
app.on("window-all-closed", () => {
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|