diff --git a/apps/web/lib/csp.ts b/apps/web/lib/csp.ts index b6625db8c4e29f..e4f6b562f2d930 100644 --- a/apps/web/lib/csp.ts +++ b/apps/web/lib/csp.ts @@ -19,9 +19,9 @@ function getCspPolicy(nonce: string) { script-src ${ IS_PRODUCTION ? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic - `'nonce-${nonce}' 'strict-dynamic' 'self' 'unsafe-inline' https:` + `'nonce-${nonce}' 'strict-dynamic' 'self' 'unsafe-inline' https: https://collector.insights.com` : // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes - "'unsafe-inline' 'unsafe-eval' https: http:" + "'unsafe-inline' 'unsafe-eval' https: http: https://collector.insights.com" }; object-src 'none'; base-uri 'none'; @@ -31,7 +31,7 @@ function getCspPolicy(nonce: string) { } app.cal.com; font-src 'self'; img-src 'self' ${WEBAPP_URL} https://img.youtube.com https://eu.ui-avatars.com/api/ data:; - connect-src 'self' + connect-src 'self' https://collector.insights.com${IS_PRODUCTION ? "" : " ws: wss:"} `; } diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index 8a16b698fb6ffa..ef9f2bd06c98bf 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -30,7 +30,7 @@ export const EventTypeAddonMap = { gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")), hitpay: dynamic(() => import("./hitpay/components/EventTypeAppCardInterface")), hubspot: dynamic(() => import("./hubspot/components/EventTypeAppCardInterface")), - insihts: dynamic(() => import("./insihts/components/EventTypeAppCardInterface")), + insights: dynamic(() => import("./insights/components/EventTypeAppCardInterface")), matomo: dynamic(() => import("./matomo/components/EventTypeAppCardInterface")), metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")), "mock-payment-app": dynamic(() => import("./mock-payment-app/components/EventTypeAppCardInterface")), diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 9cf061a4401670..c5dda9980f0efd 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -17,7 +17,7 @@ import { appKeysSchema as googlevideo_zod_ts } from "./googlevideo/zod"; import { appKeysSchema as gtm_zod_ts } from "./gtm/zod"; import { appKeysSchema as hitpay_zod_ts } from "./hitpay/zod"; import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod"; -import { appKeysSchema as insihts_zod_ts } from "./insihts/zod"; +import { appKeysSchema as insights_zod_ts } from "./insights/zod"; import { appKeysSchema as intercom_zod_ts } from "./intercom/zod"; import { appKeysSchema as jelly_zod_ts } from "./jelly/zod"; import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; @@ -68,7 +68,7 @@ export const appKeysSchemas = { gtm: gtm_zod_ts, hitpay: hitpay_zod_ts, hubspot: hubspot_zod_ts, - insihts: insihts_zod_ts, + insights: insights_zod_ts, intercom: intercom_zod_ts, jelly: jelly_zod_ts, jitsivideo: jitsivideo_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 1749ef3c95aa2a..f773b9356c5995 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -44,7 +44,7 @@ import horizon_workrooms_config_json from "./horizon-workrooms/config.json"; import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata"; import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json"; -import insihts_config_json from "./insihts/config.json"; +import insights_config_json from "./insights/config.json"; import intercom_config_json from "./intercom/config.json"; import jelly_config_json from "./jelly/config.json"; import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; @@ -152,7 +152,7 @@ export const appStoreMetadata = { hubspot: hubspot__metadata_ts, huddle01video: huddle01video__metadata_ts, "ics-feedcalendar": ics_feedcalendar_config_json, - insihts: insihts_config_json, + insights: insights_config_json, intercom: intercom_config_json, jelly: jelly_config_json, jitsivideo: jitsivideo__metadata_ts, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index f1cad4389f7374..29dba347e6eb3c 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -17,7 +17,7 @@ import { appDataSchema as googlevideo_zod_ts } from "./googlevideo/zod"; import { appDataSchema as gtm_zod_ts } from "./gtm/zod"; import { appDataSchema as hitpay_zod_ts } from "./hitpay/zod"; import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod"; -import { appDataSchema as insihts_zod_ts } from "./insihts/zod"; +import { appDataSchema as insights_zod_ts } from "./insights/zod"; import { appDataSchema as intercom_zod_ts } from "./intercom/zod"; import { appDataSchema as jelly_zod_ts } from "./jelly/zod"; import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; @@ -68,7 +68,7 @@ export const appDataSchemas = { gtm: gtm_zod_ts, hitpay: hitpay_zod_ts, hubspot: hubspot_zod_ts, - insihts: insihts_zod_ts, + insights: insights_zod_ts, intercom: intercom_zod_ts, jelly: jelly_zod_ts, jitsivideo: jitsivideo_zod_ts, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index de8307de54f744..0bc846e0d47d8d 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -44,7 +44,7 @@ export const apiHandlers = { hubspot: import("./hubspot/api"), huddle01video: import("./huddle01video/api"), "ics-feedcalendar": import("./ics-feedcalendar/api"), - insihts: import("./insihts/api"), + insights: import("./insights/api"), intercom: import("./intercom/api"), jelly: import("./jelly/api"), jitsivideo: import("./jitsivideo/api"), diff --git a/packages/app-store/bookerApps.metadata.generated.ts b/packages/app-store/bookerApps.metadata.generated.ts index c696da4f72cff7..5f830e2f73ed36 100644 --- a/packages/app-store/bookerApps.metadata.generated.ts +++ b/packages/app-store/bookerApps.metadata.generated.ts @@ -16,7 +16,7 @@ import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; import gtm_config_json from "./gtm/config.json"; import horizon_workrooms_config_json from "./horizon-workrooms/config.json"; import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; -import insihts_config_json from "./insihts/config.json"; +import insights_config_json from "./insights/config.json"; import jelly_config_json from "./jelly/config.json"; import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; import matomo_config_json from "./matomo/config.json"; @@ -61,7 +61,7 @@ export const appStoreMetadata = { gtm: gtm_config_json, "horizon-workrooms": horizon_workrooms_config_json, huddle01video: huddle01video__metadata_ts, - insihts: insihts_config_json, + insights: insights_config_json, jelly: jelly_config_json, jitsivideo: jitsivideo__metadata_ts, matomo: matomo_config_json, diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 8ddea439c5fe0d..151434bdeeaa6f 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -1,67 +1,21 @@ -const appStore = { - alby: createCachedImport(() => import("./alby")), - applecalendar: createCachedImport(() => import("./applecalendar")), - caldavcalendar: createCachedImport(() => import("./caldavcalendar")), - closecom: createCachedImport(() => import("./closecom")), - dailyvideo: createCachedImport(() => import("./dailyvideo")), - dub: createCachedImport(() => import("./dub")), - googlecalendar: createCachedImport(() => import("./googlecalendar")), - googlevideo: createCachedImport(() => import("./googlevideo")), - hubspot: createCachedImport(() => import("./hubspot")), - huddle01video: createCachedImport(() => import("./huddle01video")), - "ics-feedcalendar": createCachedImport(() => import("./ics-feedcalendar")), - jellyconferencing: createCachedImport(() => import("./jelly")), - jitsivideo: createCachedImport(() => import("./jitsivideo")), - larkcalendar: createCachedImport(() => import("./larkcalendar")), - nextcloudtalkvideo: createCachedImport(() => import("./nextcloudtalk")), - office365calendar: createCachedImport(() => import("./office365calendar")), - office365video: createCachedImport(() => import("./office365video")), - plausible: createCachedImport(() => import("./plausible")), - paypal: createCachedImport(() => import("./paypal")), - "pipedrive-crm": createCachedImport(() => import("./pipedrive-crm")), - salesforce: createCachedImport(() => import("./salesforce")), - zohocrm: createCachedImport(() => import("./zohocrm")), - sendgrid: createCachedImport(() => import("./sendgrid")), - stripepayment: createCachedImport(() => import("./stripepayment")), - tandemvideo: createCachedImport(() => import("./tandemvideo")), - vital: createCachedImport(() => import("./vital")), - zoomvideo: createCachedImport(() => import("./zoomvideo")), - wipemycalother: createCachedImport(() => import("./wipemycalother")), - webexvideo: createCachedImport(() => import("./webex")), - giphy: createCachedImport(() => import("./giphy")), - zapier: createCachedImport(() => import("./zapier")), - make: createCachedImport(() => import("./make")), - exchange2013calendar: createCachedImport(() => import("./exchange2013calendar")), - exchange2016calendar: createCachedImport(() => import("./exchange2016calendar")), - exchangecalendar: createCachedImport(() => import("./exchangecalendar")), - facetime: createCachedImport(() => import("./facetime")), - sylapsvideo: createCachedImport(() => import("./sylapsvideo")), - zohocalendar: createCachedImport(() => import("./zohocalendar")), - "zoho-bigin": createCachedImport(() => import("./zoho-bigin")), - basecamp3: createCachedImport(() => import("./basecamp3")), - telegramvideo: createCachedImport(() => import("./telegram")), - shimmervideo: createCachedImport(() => import("./shimmervideo")), - hitpay: createCachedImport(() => import("./hitpay")), - btcpayserver: createCachedImport(() => import("./btcpayserver")), -}; +/** + * Cal.com App Store - Optimized for Performance + * + * This module provides lazy loading for Cal.com apps to improve development + * performance. Instead of loading all 100+ apps upfront, apps are loaded + * on-demand when actually needed. + * + * IMPLEMENTATION CHANGE: This replaces the previous monolithic app store with + * a lazy loading system that reduces initial bundle size significantly. + * + * Backward compatibility is maintained via a compatibility proxy, though + * new code should prefer the named utilities (loadApp, hasApp, etc.). + */ +import { lazyAppStore } from "./lazy-loader"; -function createCachedImport(importFunc: () => Promise): () => Promise { - let cachedModule: T | undefined; +// Export the lazy app store as the default export +// This maintains backward compatibility with existing imports +export default lazyAppStore; - return async () => { - if (!cachedModule) { - cachedModule = await importFunc(); - } - return cachedModule; - }; -} - -const exportedAppStore: typeof appStore & { - ["mock-payment-app"]?: () => Promise; -} = appStore; - -if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { - exportedAppStore["mock-payment-app"] = createCachedImport(() => import("./mock-payment-app/index")); -} - -export default exportedAppStore; +// Re-export utilities for direct usage +export { loadApp, hasApp, getAvailableApps, preloadApps, clearAppCache } from "./lazy-loader"; diff --git a/packages/app-store/insihts/DESCRIPTION.md b/packages/app-store/insights/DESCRIPTION.md similarity index 100% rename from packages/app-store/insihts/DESCRIPTION.md rename to packages/app-store/insights/DESCRIPTION.md diff --git a/packages/app-store/insihts/api/add.ts b/packages/app-store/insights/api/add.ts similarity index 100% rename from packages/app-store/insihts/api/add.ts rename to packages/app-store/insights/api/add.ts diff --git a/packages/app-store/insihts/api/index.ts b/packages/app-store/insights/api/index.ts similarity index 100% rename from packages/app-store/insihts/api/index.ts rename to packages/app-store/insights/api/index.ts diff --git a/packages/app-store/insihts/components/EventTypeAppCardInterface.tsx b/packages/app-store/insights/components/EventTypeAppCardInterface.tsx similarity index 100% rename from packages/app-store/insihts/components/EventTypeAppCardInterface.tsx rename to packages/app-store/insights/components/EventTypeAppCardInterface.tsx diff --git a/packages/app-store/insights/config.json b/packages/app-store/insights/config.json new file mode 100644 index 00000000000000..5d02044c47cad0 --- /dev/null +++ b/packages/app-store/insights/config.json @@ -0,0 +1,31 @@ +{ + "name": "Insights", + "slug": "insights", + "type": "insights_analytics", + "logo": "icon.svg", + "url": "https://cal.com/", + "variant": "analytics", + "categories": ["analytics"], + "publisher": "Cal.com, Inc.", + "email": "help@cal.com", + "description": "Insights is an all-in-one platform for businesses looking to track user behavior, optimize workflows, and make data-driven decisions. Whether you are a marketer, product manager, or part of a customer success team, Insights provides the tools you need to succeed.", + "extendsFeature": "EventType", + "appData": { + "tag": { + "scripts": [ + { + "src": "https://collector.insights.com/script.js", + "attrs": { + "defer": true, + "crossorigin": "anonymous", + "referrerpolicy": "strict-origin-when-cross-origin", + "data-website-id": "{SITE_ID}" + } + } + ] + } + }, + "isTemplate": false, + "__createdUsingCli": true, + "__template": "booking-pages-tag" +} diff --git a/packages/app-store/insihts/index.ts b/packages/app-store/insights/index.ts similarity index 100% rename from packages/app-store/insihts/index.ts rename to packages/app-store/insights/index.ts diff --git a/packages/app-store/insights/package.json b/packages/app-store/insights/package.json new file mode 100644 index 00000000000000..55cb01695df5db --- /dev/null +++ b/packages/app-store/insights/package.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/insights", + "version": "0.0.0", + "main": "./index.ts", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*" + }, + "description": "Insights is an all-in-one platform for businesses looking to track user behavior, optimize workflows, and make data-driven decisions. Whether you are a marketer, product manager, or part of a customer success team, Insights provides the tools you need to succeed." +} diff --git a/packages/app-store/insihts/static/1.png b/packages/app-store/insights/static/1.png similarity index 100% rename from packages/app-store/insihts/static/1.png rename to packages/app-store/insights/static/1.png diff --git a/packages/app-store/insihts/static/icon.svg b/packages/app-store/insights/static/icon.svg similarity index 100% rename from packages/app-store/insihts/static/icon.svg rename to packages/app-store/insights/static/icon.svg diff --git a/packages/app-store/insihts/zod.ts b/packages/app-store/insights/zod.ts similarity index 100% rename from packages/app-store/insihts/zod.ts rename to packages/app-store/insights/zod.ts diff --git a/packages/app-store/insihts/config.json b/packages/app-store/insihts/config.json deleted file mode 100644 index 19dc95d1fd9c11..00000000000000 --- a/packages/app-store/insihts/config.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "Insihts", - "slug": "insihts", - "type": "insihts_analytics", - "logo": "icon.svg", - "url": "https://cal.com/", - "variant": "analytics", - "categories": ["analytics"], - "publisher": "Cal.com, Inc.", - "email": "help@cal.com", - "description": "Insihts is an all-in-one platform for businesses looking to track user behavior, optimize workflows, and make data-driven decisions. Whether you are a marketer, product manager, or part of a customer success team, Insihts provides the tools you need to succeed.", - "extendsFeature": "EventType", - "appData": { - "tag": { - "scripts": [ - { - "src": "https://collector.insihts.com/script.js", - "attrs": { - "data-website-id": "{SITE_ID}" - } - } - ] - } - }, - "isTemplate": false, - "__createdUsingCli": true, - "__template": "booking-pages-tag" -} diff --git a/packages/app-store/insihts/package.json b/packages/app-store/insihts/package.json deleted file mode 100644 index fa7e6bdaddd372..00000000000000 --- a/packages/app-store/insihts/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "private": true, - "name": "@calcom/insihts", - "version": "0.0.0", - "main": "./index.ts", - "dependencies": { - "@calcom/lib": "*" - }, - "devDependencies": { - "@calcom/types": "*" - }, - "description": "Insihts is an all-in-one platform for businesses looking to track user behavior, optimize workflows, and make data-driven decisions. Whether you are a marketer, product manager, or part of a customer success team, Insihts provides the tools you need to succeed." -} diff --git a/packages/app-store/lazy-loader.test.ts b/packages/app-store/lazy-loader.test.ts new file mode 100644 index 00000000000000..f4b23a80e728e4 --- /dev/null +++ b/packages/app-store/lazy-loader.test.ts @@ -0,0 +1,131 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +import { loadApp, hasApp, getAvailableApps, clearAppCache, lazyAppStore } from "./lazy-loader"; +import * as lazyLoaderModule from "./lazy-loader"; + +// Mock the dynamic imports to avoid actual module loading during tests +vi.mock("./stripepayment", () => ({ + metadata: { name: "Stripe", slug: "stripepayment" }, + api: {}, + lib: {}, +})); + +vi.mock("./googlevideo", () => ({ + metadata: { name: "Google Meet", slug: "googlevideo" }, + api: {}, + lib: {}, +})); + +describe("lazy-loader", () => { + beforeEach(() => { + clearAppCache(); + }); + + describe("loadApp", () => { + it("should load an existing app", async () => { + const app = await loadApp("stripepayment"); + expect(app).toBeDefined(); + expect(app?.metadata?.name).toBe("Stripe"); + }); + + it("should return null for non-existent app", async () => { + const app = await loadApp("does-not-exist"); + expect(app).toBeNull(); + }); + + it("should cache loaded apps", async () => { + const app1 = await loadApp("stripepayment"); + const app2 = await loadApp("stripepayment"); + expect(app1).toBe(app2); // Same reference due to caching + }); + }); + + describe("hasApp", () => { + it("should return true for existing apps", () => { + expect(hasApp("stripepayment")).toBe(true); + expect(hasApp("googlevideo")).toBe(true); + }); + + it("should return false for non-existent apps", () => { + expect(hasApp("does-not-exist")).toBe(false); + expect(hasApp("typo-app")).toBe(false); + }); + }); + + describe("getAvailableApps", () => { + it("should return array of available app names", () => { + const apps = getAvailableApps(); + expect(Array.isArray(apps)).toBe(true); + expect(apps.length).toBeGreaterThan(0); + expect(apps).toContain("stripepayment"); + expect(apps).toContain("googlevideo"); + }); + }); + + describe("lazyAppStore compatibility proxy", () => { + it("should return a function for existing apps", () => { + const loader = lazyAppStore["stripepayment"]; + expect(typeof loader).toBe("function"); + }); + + it("should return undefined for non-existent apps", () => { + const loader = lazyAppStore["does-not-exist"]; + expect(loader).toBeUndefined(); + }); + + it("should return undefined for typos", () => { + const loader = lazyAppStore["stripepaymen"]; // typo + expect(loader).toBeUndefined(); + }); + + it("should not invoke any loader for non-existent apps with optional chaining", () => { + const loadAppSpy = vi.spyOn(lazyLoaderModule, "loadApp"); + + // This should not call loadApp since the property is undefined + const result = lazyAppStore["does-not-exist"]?.(); + + expect(result).toBeUndefined(); + expect(loadAppSpy).not.toHaveBeenCalled(); + + loadAppSpy.mockRestore(); + }); + + it("should load app when calling existing app loader", async () => { + const loader = lazyAppStore["stripepayment"]; + expect(typeof loader).toBe("function"); + + const app = await loader(); + expect(app).toBeDefined(); + expect(app?.metadata?.name).toBe("Stripe"); + }); + + it("should support 'in' operator correctly", () => { + expect("stripepayment" in lazyAppStore).toBe(true); + expect("does-not-exist" in lazyAppStore).toBe(false); + }); + + it("should support Object.keys() correctly", () => { + const keys = Object.keys(lazyAppStore); + expect(Array.isArray(keys)).toBe(true); + expect(keys.length).toBeGreaterThan(0); + expect(keys).toContain("stripepayment"); + expect(keys).not.toContain("does-not-exist"); + }); + }); + + describe("clearAppCache", () => { + it("should clear the app cache", async () => { + // Load an app to cache it + const app1 = await loadApp("stripepayment"); + expect(app1).toBeDefined(); + + // Clear cache + clearAppCache(); + + // Load again - should be a fresh instance (though content will be same due to module caching) + const app2 = await loadApp("stripepayment"); + expect(app2).toBeDefined(); + expect(app2?.metadata?.name).toBe("Stripe"); + }); + }); +}); diff --git a/packages/app-store/lazy-loader.ts b/packages/app-store/lazy-loader.ts new file mode 100644 index 00000000000000..c378437513ce6a --- /dev/null +++ b/packages/app-store/lazy-loader.ts @@ -0,0 +1,233 @@ +/** + * Lazy App Store Loader + * + * This module provides a lazy loading mechanism for Cal.com apps to improve + * development performance by avoiding loading the entire app store upfront. + * + * Instead of importing all apps at once, apps are loaded on-demand when + * actually needed, reducing initial bundle size and compilation time. + */ + +type AppImportFunction = () => Promise; + +// Cache for loaded apps to avoid repeated imports +const appCache = new Map>(); + +/** + * App directory mapping - maps app names to their import functions + * This replaces the monolithic app store object + */ +const APP_IMPORT_MAP: Record = { + alby: () => import("./alby"), + amie: () => import("./amie"), + applecalendar: () => import("./applecalendar"), + attio: () => import("./attio"), + autocheckin: () => import("./autocheckin"), + "baa-for-hipaa": () => import("./baa-for-hipaa"), + basecamp3: () => import("./basecamp3"), + bolna: () => import("./bolna"), + btcpayserver: () => import("./btcpayserver"), + caldavcalendar: () => import("./caldavcalendar"), + campfire: () => import("./campfire"), + chatbase: () => import("./chatbase"), + clic: () => import("./clic"), + closecom: () => import("./closecom"), + cron: () => import("./cron"), + dailyvideo: () => import("./dailyvideo"), + deel: () => import("./deel"), + demodesk: () => import("./demodesk"), + dialpad: () => import("./dialpad"), + discord: () => import("./discord"), + dub: () => import("./dub"), + eightxeight: () => import("./eightxeight"), + "element-call": () => import("./element-call"), + elevenlabs: () => import("./elevenlabs"), + exchange2013calendar: () => import("./exchange2013calendar"), + exchange2016calendar: () => import("./exchange2016calendar"), + exchangecalendar: () => import("./exchangecalendar"), + facetime: () => import("./facetime"), + fathom: () => import("./fathom"), + feishucalendar: () => import("./feishucalendar"), + ga4: () => import("./ga4"), + giphy: () => import("./giphy"), + googlecalendar: () => import("./googlecalendar"), + googlevideo: () => import("./googlevideo"), + granola: () => import("./granola"), + "greetmate-ai": () => import("./greetmate-ai"), + gtm: () => import("./gtm"), + hitpay: () => import("./hitpay"), + "horizon-workrooms": () => import("./horizon-workrooms"), + hubspot: () => import("./hubspot"), + huddle01video: () => import("./huddle01video"), + "ics-feedcalendar": () => import("./ics-feedcalendar"), + insights: () => import("./insights"), + intercom: () => import("./intercom"), + jellyconferencing: () => import("./jelly"), + jitsivideo: () => import("./jitsivideo"), + larkcalendar: () => import("./larkcalendar"), + lindy: () => import("./lindy"), + linear: () => import("./linear"), + make: () => import("./make"), + matomo: () => import("./matomo"), + metapixel: () => import("./metapixel"), + "millis-ai": () => import("./millis-ai"), + mirotalk: () => import("./mirotalk"), + monobot: () => import("./monobot"), + n8n: () => import("./n8n"), + nextcloudtalkvideo: () => import("./nextcloudtalk"), + office365calendar: () => import("./office365calendar"), + office365video: () => import("./office365video"), + paypal: () => import("./paypal"), + ping: () => import("./ping"), + pipedream: () => import("./pipedream"), + "pipedrive-crm": () => import("./pipedrive-crm"), + plausible: () => import("./plausible"), + posthog: () => import("./posthog"), + qr_code: () => import("./qr_code"), + raycast: () => import("./raycast"), + "retell-ai": () => import("./retell-ai"), + riverside: () => import("./riverside"), + roam: () => import("./roam"), + "routing-forms": () => import("./routing-forms"), + salesforce: () => import("./salesforce"), + salesroom: () => import("./salesroom"), + sendgrid: () => import("./sendgrid"), + shimmervideo: () => import("./shimmervideo"), + signal: () => import("./signal"), + sirius_video: () => import("./sirius_video"), + skype: () => import("./skype"), + stripepayment: () => import("./stripepayment"), + sylapsvideo: () => import("./sylapsvideo"), + synthflow: () => import("./synthflow"), + tandemvideo: () => import("./tandemvideo"), + telegramvideo: () => import("./telegram"), + telli: () => import("./telli"), + twipla: () => import("./twipla"), + umami: () => import("./umami"), + vimcal: () => import("./vimcal"), + vital: () => import("./vital"), + weather_in_your_calendar: () => import("./weather_in_your_calendar"), + webexvideo: () => import("./webex"), + whatsapp: () => import("./whatsapp"), + whereby: () => import("./whereby"), + wipemycalother: () => import("./wipemycalother"), + wordpress: () => import("./wordpress"), + zapier: () => import("./zapier"), + "zoho-bigin": () => import("./zoho-bigin"), + zohocalendar: () => import("./zohocalendar"), + zohocrm: () => import("./zohocrm"), + zoomvideo: () => import("./zoomvideo"), +}; + +/** + * Loads an app by name with caching + * @param appName - The name of the app to load + * @returns Promise that resolves to the app module + */ +export async function loadApp(appName: string): Promise { + // Check cache first + if (appCache.has(appName)) { + return appCache.get(appName)!; + } + + // Check if app exists in our mapping + const importFn = APP_IMPORT_MAP[appName]; + if (!importFn) { + console.warn(`App "${appName}" not found in app store`); + return null; + } + + // Load and cache the app + const appPromise = importFn(); + appCache.set(appName, appPromise); + + try { + return await appPromise; + } catch (error) { + // Remove from cache if loading failed + appCache.delete(appName); + console.error(`Failed to load app "${appName}":`, error); + return null; + } +} + +/** + * Checks if an app exists without loading it + * @param appName - The name of the app to check + * @returns boolean indicating if the app exists + */ +export function hasApp(appName: string): boolean { + return appName in APP_IMPORT_MAP; +} + +/** + * Gets all available app names + * @returns Array of app names + */ +export function getAvailableApps(): string[] { + return Object.keys(APP_IMPORT_MAP); +} + +/** + * Preloads multiple apps (useful for critical apps) + * @param appNames - Array of app names to preload + * @returns Promise that resolves when all apps are loaded + */ +export async function preloadApps(appNames: string[]): Promise { + const loadPromises = appNames.filter(hasApp).map((appName) => loadApp(appName)); + + await Promise.allSettled(loadPromises); +} + +/** + * Clears the app cache (useful for testing) + */ +export function clearAppCache(): void { + appCache.clear(); +} + +/** + * Backward compatibility layer - mimics the old app store interface + * This allows existing code to work without changes while using lazy loading + */ +export const lazyAppStore = new Proxy({} as Record, { + get(target, prop: string) { + if (typeof prop !== "string") { + return undefined; + } + + // Only return a loader function if the app exists + if (!hasApp(prop)) { + return undefined; + } + + // Return a function that loads the app when called + return () => loadApp(prop); + }, + + has(target, prop: string) { + return hasApp(prop); + }, + + ownKeys(target) { + return getAvailableApps(); + }, + + getOwnPropertyDescriptor(target, prop: string) { + if (hasApp(prop)) { + return { + enumerable: true, + configurable: true, + value: () => loadApp(prop), + }; + } + return undefined; + }, +}); + +// Handle mock payment app for development +if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { + (APP_IMPORT_MAP as any)["mock-payment-app"] = () => import("./mock-payment-app/index"); +} + +export default lazyAppStore; diff --git a/packages/app-store/types.d.ts b/packages/app-store/types.d.ts index c333aeac0ceab5..68ee2e05ea1ec0 100644 --- a/packages/app-store/types.d.ts +++ b/packages/app-store/types.d.ts @@ -5,6 +5,8 @@ import type { EventTypeFormMetadataSchema } from "@calcom/prisma/zod-utils"; import type { RouterOutputs } from "@calcom/trpc/react"; import type { ButtonProps } from "@calcom/ui/components/button"; +import type { GetAppData, SetAppData } from "./EventTypeAppContext"; + export type IntegrationOAuthCallbackState = { returnTo?: string; onErrorReturnTo: string; @@ -48,7 +50,7 @@ export interface InstallAppButtonProps { export type EventTypeAppCardComponentProps = { // Limit what data should be accessible to apps eventType: Pick< - z.infer, + z.infer, | "id" | "title" | "description" @@ -70,7 +72,7 @@ export type EventTypeAppCardComponentProps = { export type EventTypeAppSettingsComponentProps = { // Limit what data should be accessible to apps\ eventType: Pick< - z.infer, + z.infer, "id" | "title" | "description" | "teamId" | "length" | "recurringEvent" | "seatsPerTimeSlot" | "team" > & { URL: string; @@ -85,4 +87,5 @@ export type EventTypeAppCardComponent = React.FC export type EventTypeAppSettingsComponent = React.FC; -export type EventTypeModel = z.infer; +// Remove circular reference - EventTypeModel should be imported from prisma +// export type EventTypeModel = z.infer; diff --git a/packages/lib/getConnectedApps.ts b/packages/lib/getConnectedApps.ts index 09567d64283b7f..2ec9d5578cf9f2 100644 --- a/packages/lib/getConnectedApps.ts +++ b/packages/lib/getConnectedApps.ts @@ -1,6 +1,6 @@ import type { Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { loadApp } from "@calcom/app-store"; import type { TDependencyData } from "@calcom/app-store/_appRegistry"; import type { CredentialOwner } from "@calcom/app-store/types"; import { getAppFromSlug } from "@calcom/app-store/utils"; @@ -184,9 +184,9 @@ export async function getConnectedApps({ // undefined it means that app don't require app/setup/page let isSetupAlready = undefined; if (credential && app.categories.includes("payment")) { - const paymentApp = (await appStore[app.dirName as keyof typeof appStore]?.()) as PaymentApp | null; + const paymentApp = (await loadApp(app.dirName || "")) as PaymentApp | null; if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) { - const PaymentService = paymentApp.lib.PaymentService; + const PaymentService = paymentApp.lib!.PaymentService; const paymentInstance = new PaymentService(credential); isSetupAlready = paymentInstance.isSetupAlready(); } diff --git a/packages/lib/payment/deletePayment.ts b/packages/lib/payment/deletePayment.ts index 20411bdf79c37b..0be6be74790864 100644 --- a/packages/lib/payment/deletePayment.ts +++ b/packages/lib/payment/deletePayment.ts @@ -1,6 +1,6 @@ import type { Payment, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { loadApp } from "@calcom/app-store"; import type { AppCategories } from "@calcom/prisma/enums"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -15,15 +15,13 @@ const deletePayment = async ( } | null; } ): Promise => { - const paymentApp = (await appStore[ - paymentAppCredentials?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp; + const paymentApp = (await loadApp(paymentAppCredentials?.app?.dirName || "")) as PaymentApp; if (!paymentApp?.lib?.PaymentService) { console.warn(`payment App service of type ${paymentApp} is not implemented`); return false; } // eslint-disable-next-line @typescript-eslint/no-explicit-any - const PaymentService = paymentApp.lib.PaymentService as any; + const PaymentService = paymentApp.lib!.PaymentService as any; const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService; const deleted = await paymentInstance.deletePayment(paymentId); return deleted; diff --git a/packages/lib/payment/handlePayment.ts b/packages/lib/payment/handlePayment.ts index 536ecf73cd3f57..e2d3203c2add15 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -1,6 +1,6 @@ import type { AppCategories, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { loadApp } from "@calcom/app-store"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; import type { CompleteEventType } from "@calcom/prisma/zod"; import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils"; @@ -15,9 +15,6 @@ const isPaymentApp = (x: unknown): x is PaymentApp => !!x.lib && "PaymentService" in x.lib; -const isKeyOf = (obj: T, key: unknown): key is keyof T => - typeof key === "string" && key in obj; - const handlePayment = async ({ evt, selectedEventType, @@ -52,16 +49,18 @@ const handlePayment = async ({ }) => { if (isDryRun) return null; const key = paymentAppCredentials?.app?.dirName; - if (!isKeyOf(appStore, key)) { - console.warn(`key: ${key} is not a valid key in appStore`); + if (!key) { + console.warn(`No app dirName found in payment credentials`); return null; } - const paymentApp = await appStore[key]?.(); + + // Use lazy loading instead of importing the entire app store + const paymentApp = await loadApp(key); if (!isPaymentApp(paymentApp)) { console.warn(`payment App service of type ${paymentApp} is not implemented`); return null; } - const PaymentService = paymentApp.lib.PaymentService; + const PaymentService = paymentApp.lib!.PaymentService; const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService; const apps = eventTypeAppMetadataOptionalSchema.parse(selectedEventType?.metadata?.apps); diff --git a/packages/lib/payment/handlePaymentRefund.ts b/packages/lib/payment/handlePaymentRefund.ts index a51a12423ed15f..95596542233b5b 100644 --- a/packages/lib/payment/handlePaymentRefund.ts +++ b/packages/lib/payment/handlePaymentRefund.ts @@ -1,6 +1,6 @@ import type { Payment, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { loadApp } from "@calcom/app-store"; import type { AppCategories } from "@calcom/prisma/enums"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -15,15 +15,13 @@ const handlePaymentRefund = async ( } | null; } ) => { - const paymentApp = (await appStore[ - paymentAppCredentials?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp; + const paymentApp = (await loadApp(paymentAppCredentials?.app?.dirName || "")) as PaymentApp; if (!paymentApp?.lib?.PaymentService) { console.warn(`payment App service of type ${paymentApp} is not implemented`); return false; } // eslint-disable-next-line @typescript-eslint/no-explicit-any - const PaymentService = paymentApp.lib.PaymentService as any; + const PaymentService = paymentApp.lib!.PaymentService as any; const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService; const refund = await paymentInstance.refund(paymentId); return refund; diff --git a/packages/lib/videoClient.ts b/packages/lib/videoClient.ts index 1b4c51b505cc90..0109f88d2de882 100644 --- a/packages/lib/videoClient.ts +++ b/packages/lib/videoClient.ts @@ -1,7 +1,7 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; -import appStore from "@calcom/app-store"; +import { loadApp } from "@calcom/app-store"; import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys"; import { DailyLocationType } from "@calcom/app-store/locations"; import { sendBrokenIntegrationEmail } from "@calcom/emails"; @@ -27,10 +27,9 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise