diff --git a/internal-packages/billing/src/index.ts b/internal-packages/billing/src/index.ts index 321609445dc..3d38bb25f66 100644 --- a/internal-packages/billing/src/index.ts +++ b/internal-packages/billing/src/index.ts @@ -103,8 +103,11 @@ export class LazyController implements BillingController { const code = (error as NodeJS.ErrnoException | undefined)?.code; const message = error instanceof Error ? error.message : String(error); const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; + const isPluginItselfMissing = + isModuleNotFound && + (message.includes(MODULE_NAME) || message.includes("'@triggerdotdev/plugins'")); - if (!isModuleNotFound || !message.includes(MODULE_NAME)) { + if (!isPluginItselfMissing) { console.error( "Billing: plugin found but failed to load; falling back to default implementation", error diff --git a/internal-packages/billing/src/loader.test.ts b/internal-packages/billing/src/loader.test.ts index 4aeb7c4b9e7..b620afd7f93 100644 --- a/internal-packages/billing/src/loader.test.ts +++ b/internal-packages/billing/src/loader.test.ts @@ -130,16 +130,34 @@ describe("billing loader", () => { }); it("falls back when the plugin is not installed", async () => { - const controller = new LazyController({ - importer: async () => { - throw moduleNotFound("@triggerdotdev/plugins/billing"); - }, - }); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const controller = new LazyController({ + importer: async () => { + throw moduleNotFound("@triggerdotdev/plugins/billing"); + }, + }); - await expect(controller.isUsingPlugin()).resolves.toBe(false); + await expect(controller.isUsingPlugin()).resolves.toBe(false); - const result = await controller.provisionCustomer({ organizationId: "org_1", name: "Acme" }); - expect(result._unsafeUnwrapErr()).toBe("feature_disabled"); + const result = await controller.provisionCustomer({ organizationId: "org_1", name: "Acme" }); + expect(result._unsafeUnwrapErr()).toBe("feature_disabled"); + expect(consoleError).not.toHaveBeenCalled(); + + const barePkgController = new LazyController({ + importer: async () => { + const error: NodeJS.ErrnoException = new Error( + "Cannot find package '@triggerdotdev/plugins' imported from /triggerdotdev/apps/webapp/build/server/index.js" + ); + error.code = "ERR_MODULE_NOT_FOUND"; + throw error; + }, + }); + await expect(barePkgController.isUsingPlugin()).resolves.toBe(false); + expect(consoleError).not.toHaveBeenCalled(); + } finally { + consoleError.mockRestore(); + } }); it("falls back loudly when the plugin is present but fails to initialise", async () => { diff --git a/internal-packages/rbac/src/index.ts b/internal-packages/rbac/src/index.ts index cf46c5d0907..969cbf859b8 100644 --- a/internal-packages/rbac/src/index.ts +++ b/internal-packages/rbac/src/index.ts @@ -194,7 +194,9 @@ class LazyController implements RoleBaseAccessController { const code = (err as NodeJS.ErrnoException | undefined)?.code; const message = err instanceof Error ? err.message : String(err); const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; - const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName); + const isPluginItselfMissing = + isModuleNotFound && + (message.includes(moduleName) || message.includes("'@triggerdotdev/plugins'")); if (!isPluginItselfMissing) { // Either the error wasn't a missing-module error at all, or the diff --git a/internal-packages/rbac/src/require-plugins.test.ts b/internal-packages/rbac/src/require-plugins.test.ts index 0b81674f536..56e9a4a4581 100644 --- a/internal-packages/rbac/src/require-plugins.test.ts +++ b/internal-packages/rbac/src/require-plugins.test.ts @@ -20,8 +20,15 @@ describe("LazyController plugin loading", () => { it("falls back silently when REQUIRE_PLUGINS is unset and the plugin is missing", async () => { vi.stubEnv("REQUIRE_PLUGINS", ""); - const controller = loader.create(prismaPlaceholder); - await expect(controller.isUsingPlugin()).resolves.toBe(false); + vi.stubEnv("RBAC_LOG_FALLBACK", ""); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const controller = loader.create(prismaPlaceholder); + await expect(controller.isUsingPlugin()).resolves.toBe(false); + expect(errorSpy).not.toHaveBeenCalled(); + } finally { + errorSpy.mockRestore(); + } }); it("throws when REQUIRE_PLUGINS=1 and the plugin is missing", async () => { diff --git a/internal-packages/sso/src/index.ts b/internal-packages/sso/src/index.ts index b4c27e70263..101a44d8bf3 100644 --- a/internal-packages/sso/src/index.ts +++ b/internal-packages/sso/src/index.ts @@ -82,7 +82,9 @@ export class LazyController implements SsoController { const code = (err as NodeJS.ErrnoException | undefined)?.code; const message = err instanceof Error ? err.message : String(err); const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; - const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName); + const isPluginItselfMissing = + isModuleNotFound && + (message.includes(moduleName) || message.includes("'@triggerdotdev/plugins'")); if (!isPluginItselfMissing) { console.error( diff --git a/internal-packages/sso/src/loader.test.ts b/internal-packages/sso/src/loader.test.ts index d5e75b4d59d..1f747fe4b23 100644 --- a/internal-packages/sso/src/loader.test.ts +++ b/internal-packages/sso/src/loader.test.ts @@ -167,6 +167,31 @@ describe("SSO LazyController", () => { } }); + it("stays silent when Node ESM reports the missing bare package '@triggerdotdev/plugins'", async () => { + const previous = process.env.SSO_LOG_FALLBACK; + delete process.env.SSO_LOG_FALLBACK; + const importer = vi.fn(async () => { + const err = Object.assign( + new Error( + "Cannot find package '@triggerdotdev/plugins' imported from /triggerdotdev/apps/webapp/build/server/index.js" + ), + { code: "ERR_MODULE_NOT_FOUND" } + ); + throw err; + }); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); + + try { + const controller = new LazyController(fakePrisma, { importer }); + expect(await controller.isUsingPlugin()).toBe(false); + expect(errorSpy).not.toHaveBeenCalled(); + } finally { + errorSpy.mockRestore(); + if (previous === undefined) delete process.env.SSO_LOG_FALLBACK; + else process.env.SSO_LOG_FALLBACK = previous; + } + }); + it("logs an info line when SSO_LOG_FALLBACK=1", async () => { const previous = process.env.SSO_LOG_FALLBACK; process.env.SSO_LOG_FALLBACK = "1";