From d43f543c593dcd63db2a2b376ac85a5c90e70245 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Thu, 29 Oct 2026 14:01:14 -0500 Subject: [PATCH] fix: let commonjs plugins fall back to core packages on the desktop --- CHANGELOG.md | 1 + src/backend/plugins/host-modules.ts | 23 ++++++++++++-- .../tests/plugins/host-modules.test.ts | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57960c845..5766454a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Cmd+1 (Alt+1 on Windows and Linux) and the other number shortcuts pick the open tabs as numbered in the tab bar, not the home tab - A plugin the app no longer ships and could not download (for example on a locked down network) shows as failed with the reason on the Plugins tab, instead of as installed - The desktop app no longer shows This layout could not be shown when the layout is asked for before its plugin has finished starting; it tries again +- On the desktop, a plugin installed from the registry that uses core's packages through require (like ACME SSL and axios) starts instead of failing with Cannot find module ## 26.10.5 diff --git a/src/backend/plugins/host-modules.ts b/src/backend/plugins/host-modules.ts index 6aa3f01c9..bbfdb943f 100644 --- a/src/backend/plugins/host-modules.ts +++ b/src/backend/plugins/host-modules.ts @@ -38,6 +38,20 @@ export function isUserPluginFile( ); } +/** Whether a failed import from a plugin should try core's copy. */ +export function shouldUseHostModule( + error: unknown, + specifier: string, + parentURL: string | undefined, + pluginsDir: string, +): boolean { + const code = (error as { code?: string })?.code; + // import() says ERR_MODULE_NOT_FOUND, a CommonJS require MODULE_NOT_FOUND. + if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") + return false; + return isBareSpecifier(specifier) && isUserPluginFile(parentURL, pluginsDir); +} + export function installHostModuleFallback(): void { if (installed) return; installed = true; @@ -49,9 +63,12 @@ export function installHostModuleFallback(): void { return nextResolve(specifier, context); } catch (error) { if ( - (error as { code?: string })?.code !== "ERR_MODULE_NOT_FOUND" || - !isBareSpecifier(specifier) || - !isUserPluginFile(context.parentURL, getPluginsDir()) + !shouldUseHostModule( + error, + specifier, + context.parentURL, + getPluginsDir(), + ) ) { throw error; } diff --git a/src/backend/tests/plugins/host-modules.test.ts b/src/backend/tests/plugins/host-modules.test.ts index 9620502f7..429ac85a6 100644 --- a/src/backend/tests/plugins/host-modules.test.ts +++ b/src/backend/tests/plugins/host-modules.test.ts @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { isBareSpecifier, isUserPluginFile, + shouldUseHostModule, } from "../../plugins/host-modules.js"; describe("isBareSpecifier", () => { @@ -46,6 +47,36 @@ describe("isUserPluginFile", () => { }); }); +describe("shouldUseHostModule", () => { + const root = path.resolve(os.tmpdir(), "termix-data", "plugins"); + const parent = pathToFileURL( + path.join(root, ".unpacked", "demo", "dist", "backend.js"), + ).href; + + it("falls back for import and require misses from a plugin", () => { + for (const code of ["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND"]) { + expect(shouldUseHostModule({ code }, "axios", parent, root)).toBe(true); + } + }); + + it("leaves other errors and files alone", () => { + expect( + shouldUseHostModule({ code: "ERR_REQUIRE_ESM" }, "axios", parent, root), + ).toBe(false); + expect( + shouldUseHostModule({ code: "MODULE_NOT_FOUND" }, "./x.js", parent, root), + ).toBe(false); + expect( + shouldUseHostModule( + { code: "MODULE_NOT_FOUND" }, + "axios", + undefined, + root, + ), + ).toBe(false); + }); +}); + describe("installHostModuleFallback", () => { let dataDir: string | null = null;