From e43a89096dfe3820902562580f6a470b5849ec5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 22 Apr 2026 19:16:22 -0400 Subject: [PATCH] refactor(main): audit warning suppression rules --- src/main/index.ts | 2 +- src/main/install-main-warning-suppressions.ts | 18 ++++++ src/main/main-warning-suppression-rules.ts | 53 +++++++++++++++++ src/main/suppress-warnings.ts | 58 ------------------- .../main-warning-suppression-rules.test.ts | 44 ++++++++++++++ 5 files changed, 116 insertions(+), 59 deletions(-) create mode 100644 src/main/install-main-warning-suppressions.ts create mode 100644 src/main/main-warning-suppression-rules.ts delete mode 100644 src/main/suppress-warnings.ts create mode 100644 tests/unit/main-warning-suppression-rules.test.ts diff --git a/src/main/index.ts b/src/main/index.ts index bcd2a1da..fe4688e2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,4 @@ -import "./suppress-warnings"; +import "./install-main-warning-suppressions"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { app } from "electron"; diff --git a/src/main/install-main-warning-suppressions.ts b/src/main/install-main-warning-suppressions.ts new file mode 100644 index 00000000..dc874683 --- /dev/null +++ b/src/main/install-main-warning-suppressions.ts @@ -0,0 +1,18 @@ +import { + classifyConsoleErrorSuppression, + classifyWarningSuppression, +} from "./main-warning-suppression-rules"; + +const originalEmitWarning = process.emitWarning.bind(process); + +process.emitWarning = ((warning: unknown, ...rest: unknown[]) => { + if (classifyWarningSuppression(warning)) return; + return (originalEmitWarning as (...args: unknown[]) => void)(warning, ...rest); +}) as typeof process.emitWarning; + +const originalConsoleError = console.error.bind(console); + +console.error = ((...args: unknown[]) => { + if (classifyConsoleErrorSuppression(args)) return; + return originalConsoleError(...args); +}) as typeof console.error; diff --git a/src/main/main-warning-suppression-rules.ts b/src/main/main-warning-suppression-rules.ts new file mode 100644 index 00000000..f8e0ad59 --- /dev/null +++ b/src/main/main-warning-suppression-rules.ts @@ -0,0 +1,53 @@ +export type WarningSuppressionCategory = + | "node-sqlite-experimental-warning" + | "electron-disposed-web-frame-send"; + +interface SuppressionRule { + category: WarningSuppressionCategory; + reason: string; + matches: (input: TInput) => boolean; +} + +export const WARNING_SUPPRESSION_RULES: ReadonlyArray> = [ + { + category: "node-sqlite-experimental-warning", + reason: "Node emits this before the local SQLite profile store can run; the runtime version pins the API surface.", + matches: isSqliteExperimentalWarning, + }, +]; + +export const CONSOLE_ERROR_SUPPRESSION_RULES: ReadonlyArray> = [ + { + category: "electron-disposed-web-frame-send", + reason: "Electron logs a send to a disposed renderer frame during close, reload, or HMR after the send has become a no-op.", + matches: isDisposedWebFrameSend, + }, +]; + +export function classifyWarningSuppression(warning: unknown): WarningSuppressionCategory | null { + return WARNING_SUPPRESSION_RULES.find((rule) => rule.matches(warning))?.category ?? null; +} + +export function classifyConsoleErrorSuppression(args: unknown[]): WarningSuppressionCategory | null { + return CONSOLE_ERROR_SUPPRESSION_RULES.find((rule) => rule.matches(args))?.category ?? null; +} + +function isSqliteExperimentalWarning(warning: unknown): boolean { + if (typeof warning === "string") { + return /SQLite is an experimental feature/.test(warning); + } + if (warning instanceof Error) { + return warning.name === "ExperimentalWarning" && /SQLite/.test(warning.message); + } + return false; +} + +function isDisposedWebFrameSend(args: unknown[]): boolean { + if (args.length === 0) return false; + const first = args[0]; + if (typeof first !== "string") return false; + if (!first.startsWith("Error sending from webFrameMain")) return false; + const payload = args[1]; + const message = payload instanceof Error ? payload.message : String(payload ?? ""); + return message.includes("Render frame was disposed"); +} diff --git a/src/main/suppress-warnings.ts b/src/main/suppress-warnings.ts deleted file mode 100644 index 89250e22..00000000 --- a/src/main/suppress-warnings.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Silences two Electron/Node sources of stderr spam that are pure - * lifecycle noise, not logic bugs: - * - * 1. `node:sqlite` ExperimentalWarning on first import — emitted by the - * Node runtime itself. Must be installed before any module touches - * `node:sqlite`, or the warning has already fired. - * - * 2. `Error sending from webFrameMain:` — emitted by Electron's own - * `WebFrameMain.prototype.send` when the renderer frame was disposed - * mid-send (window close, dev HMR, reload). Electron catches the - * native exception internally and `console.error`s it; the send is - * already a no-op by the time our `webContents.send` returns, and - * nothing downstream reads the message. With every agent-event fan - * out producing two sends, a single dead renderer floods the log - * with thousands of copies and can stall the main event loop on - * synchronous stderr I/O. We filter only this specific prefix so - * real console errors still surface. - * - * Targeted: everything else passes through unchanged. - */ - -const originalEmitWarning = process.emitWarning.bind(process); - -function isSqliteExperimentalWarning(warning: unknown): boolean { - if (typeof warning === "string") { - return /SQLite is an experimental feature/.test(warning); - } - if (warning instanceof Error) { - return warning.name === "ExperimentalWarning" && /SQLite/.test(warning.message); - } - return false; -} - -process.emitWarning = ((warning: unknown, ...rest: unknown[]) => { - if (isSqliteExperimentalWarning(warning)) return; - return (originalEmitWarning as (...args: unknown[]) => void)(warning, ...rest); -}) as typeof process.emitWarning; - -const originalConsoleError = console.error.bind(console); - -function isDisposedWebFrameNoise(args: unknown[]): boolean { - if (args.length === 0) return false; - const first = args[0]; - if (typeof first !== "string") return false; - if (!first.startsWith("Error sending from webFrameMain")) return false; - // Only swallow when the accompanying error is the disposal signal; - // any other payload surfaces normally so genuine IPC failures still - // show up in logs. - const payload = args[1]; - const message = payload instanceof Error ? payload.message : String(payload ?? ""); - return message.includes("Render frame was disposed"); -} - -console.error = ((...args: unknown[]) => { - if (isDisposedWebFrameNoise(args)) return; - return originalConsoleError(...args); -}) as typeof console.error; diff --git a/tests/unit/main-warning-suppression-rules.test.ts b/tests/unit/main-warning-suppression-rules.test.ts new file mode 100644 index 00000000..39bbbc41 --- /dev/null +++ b/tests/unit/main-warning-suppression-rules.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { + CONSOLE_ERROR_SUPPRESSION_RULES, + WARNING_SUPPRESSION_RULES, + classifyConsoleErrorSuppression, + classifyWarningSuppression, +} from "../../src/main/main-warning-suppression-rules"; + +describe("main warning suppression rules", () => { + it("documents every warning suppression category with an operator reason", () => { + for (const rule of [...WARNING_SUPPRESSION_RULES, ...CONSOLE_ERROR_SUPPRESSION_RULES]) { + expect(rule.category).toMatch(/^[a-z0-9-]+$/); + expect(rule.reason.length).toBeGreaterThan(40); + } + }); + + it("suppresses only the node sqlite experimental warning", () => { + const sqliteWarning = new Error("SQLite is an experimental feature and might change at any time"); + sqliteWarning.name = "ExperimentalWarning"; + + const genericWarning = new Error("fetch is an experimental feature"); + genericWarning.name = "ExperimentalWarning"; + + expect(classifyWarningSuppression(sqliteWarning)).toBe("node-sqlite-experimental-warning"); + expect(classifyWarningSuppression("SQLite is an experimental feature and might change at any time")) + .toBe("node-sqlite-experimental-warning"); + expect(classifyWarningSuppression(genericWarning)).toBeNull(); + expect(classifyWarningSuppression("deprecation warning")).toBeNull(); + }); + + it("suppresses only disposed renderer frame send noise", () => { + expect(classifyConsoleErrorSuppression([ + "Error sending from webFrameMain:", + new Error("Render frame was disposed before WebFrameMain could send"), + ])).toBe("electron-disposed-web-frame-send"); + + expect(classifyConsoleErrorSuppression([ + "Error sending from webFrameMain:", + new Error("Permission denied"), + ])).toBeNull(); + expect(classifyConsoleErrorSuppression(["Unhandled renderer error", new Error("Render frame was disposed")])) + .toBeNull(); + }); +});