diff --git a/openclaw.mjs b/openclaw.mjs index 686593fb22..6649f4e81c 100755 --- a/openclaw.mjs +++ b/openclaw.mjs @@ -11,13 +11,36 @@ if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) { } } +const isModuleNotFoundError = (err) => + err && typeof err === "object" && "code" in err && err.code === "ERR_MODULE_NOT_FOUND"; + +const installProcessWarningFilter = async () => { + // Keep bootstrap warnings consistent with the TypeScript runtime. + for (const specifier of ["./dist/warning-filter.js", "./dist/warning-filter.mjs"]) { + try { + const mod = await import(specifier); + if (typeof mod.installProcessWarningFilter === "function") { + mod.installProcessWarningFilter(); + return; + } + } catch (err) { + if (isModuleNotFoundError(err)) { + continue; + } + throw err; + } + } +}; + +await installProcessWarningFilter(); + const tryImport = async (specifier) => { try { await import(specifier); return true; } catch (err) { // Only swallow missing-module errors; rethrow real runtime errors. - if (err && typeof err === "object" && "code" in err && err.code === "ERR_MODULE_NOT_FOUND") { + if (isModuleNotFoundError(err)) { return false; } throw err; diff --git a/src/infra/warning-filter.test.ts b/src/infra/warning-filter.test.ts new file mode 100644 index 0000000000..8220037d95 --- /dev/null +++ b/src/infra/warning-filter.test.ts @@ -0,0 +1,89 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { installProcessWarningFilter, shouldIgnoreWarning } from "./warning-filter.js"; + +const warningFilterKey = Symbol.for("openclaw.warning-filter"); + +function resetWarningFilterInstallState(): void { + const globalState = globalThis as typeof globalThis & { + [warningFilterKey]?: { installed: boolean }; + }; + delete globalState[warningFilterKey]; +} + +describe("warning filter", () => { + beforeEach(() => { + resetWarningFilterInstallState(); + }); + + afterEach(() => { + resetWarningFilterInstallState(); + vi.restoreAllMocks(); + }); + + it("suppresses known deprecation and experimental warning signatures", () => { + expect( + shouldIgnoreWarning({ + name: "DeprecationWarning", + code: "DEP0040", + message: "The punycode module is deprecated.", + }), + ).toBe(true); + expect( + shouldIgnoreWarning({ + name: "DeprecationWarning", + code: "DEP0060", + message: "The `util._extend` API is deprecated.", + }), + ).toBe(true); + expect( + shouldIgnoreWarning({ + name: "ExperimentalWarning", + message: "SQLite is an experimental feature and might change at any time", + }), + ).toBe(true); + }); + + it("keeps unknown warnings visible", () => { + expect( + shouldIgnoreWarning({ + name: "DeprecationWarning", + code: "DEP9999", + message: "Totally new warning", + }), + ).toBe(false); + }); + + it("installs once and only writes unsuppressed warnings", () => { + let warningHandler: ((warning: Error & { code?: string; message?: string }) => void) | null = + null; + const onSpy = vi.spyOn(process, "on").mockImplementation(((event, handler) => { + if (event === "warning") { + warningHandler = handler as (warning: Error & { code?: string; message?: string }) => void; + } + return process; + }) as typeof process.on); + const writeSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); + + installProcessWarningFilter(); + installProcessWarningFilter(); + + expect(onSpy).toHaveBeenCalledTimes(1); + expect(warningHandler).not.toBeNull(); + + warningHandler?.({ + name: "DeprecationWarning", + code: "DEP0060", + message: "The `util._extend` API is deprecated.", + toString: () => "suppressed", + } as Error & { code?: string; message?: string }); + expect(writeSpy).not.toHaveBeenCalled(); + + warningHandler?.({ + name: "Warning", + message: "Visible warning", + stack: "Warning: visible", + toString: () => "visible", + } as Error & { code?: string; message?: string }); + expect(writeSpy).toHaveBeenCalledWith("Warning: visible\n"); + }); +}); diff --git a/src/infra/warning-filter.ts b/src/infra/warning-filter.ts new file mode 100644 index 0000000000..68af7ad5d3 --- /dev/null +++ b/src/infra/warning-filter.ts @@ -0,0 +1,40 @@ +const warningFilterKey = Symbol.for("openclaw.warning-filter"); + +export type ProcessWarning = Error & { + code?: string; + name?: string; + message?: string; +}; + +export function shouldIgnoreWarning(warning: ProcessWarning): boolean { + if (warning.code === "DEP0040" && warning.message?.includes("punycode")) { + return true; + } + if (warning.code === "DEP0060" && warning.message?.includes("util._extend")) { + return true; + } + if ( + warning.name === "ExperimentalWarning" && + warning.message?.includes("SQLite is an experimental feature") + ) { + return true; + } + return false; +} + +export function installProcessWarningFilter(): void { + const globalState = globalThis as typeof globalThis & { + [warningFilterKey]?: { installed: boolean }; + }; + if (globalState[warningFilterKey]?.installed) { + return; + } + globalState[warningFilterKey] = { installed: true }; + + process.on("warning", (warning: ProcessWarning) => { + if (shouldIgnoreWarning(warning)) { + return; + } + process.stderr.write(`${warning.stack ?? warning.toString()}\n`); + }); +} diff --git a/src/infra/warnings.ts b/src/infra/warnings.ts index 91a98b0f32..e8b048ddb9 100644 --- a/src/infra/warnings.ts +++ b/src/infra/warnings.ts @@ -1,40 +1 @@ -const warningFilterKey = Symbol.for("openclaw.warning-filter"); - -type Warning = Error & { - code?: string; - name?: string; - message?: string; -}; - -function shouldIgnoreWarning(warning: Warning): boolean { - if (warning.code === "DEP0040" && warning.message?.includes("punycode")) { - return true; - } - if (warning.code === "DEP0060" && warning.message?.includes("util._extend")) { - return true; - } - if ( - warning.name === "ExperimentalWarning" && - warning.message?.includes("SQLite is an experimental feature") - ) { - return true; - } - return false; -} - -export function installProcessWarningFilter(): void { - const globalState = globalThis as typeof globalThis & { - [warningFilterKey]?: { installed: boolean }; - }; - if (globalState[warningFilterKey]?.installed) { - return; - } - globalState[warningFilterKey] = { installed: true }; - - process.on("warning", (warning: Warning) => { - if (shouldIgnoreWarning(warning)) { - return; - } - process.stderr.write(`${warning.stack ?? warning.toString()}\n`); - }); -} +export { installProcessWarningFilter } from "./warning-filter.js"; diff --git a/tsdown.config.ts b/tsdown.config.ts index d4c11bd53f..232ab79592 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -17,6 +17,12 @@ export default defineConfig([ fixedExtension: false, platform: "node", }, + { + entry: "src/infra/warning-filter.ts", + env, + fixedExtension: false, + platform: "node", + }, { entry: "src/plugin-sdk/index.ts", outDir: "dist/plugin-sdk",