From 30e5c47bc1c834ee617c4075d52d0c43ca1af517 Mon Sep 17 00:00:00 2001 From: thetanav Date: Sat, 4 Oct 2025 19:46:52 +0530 Subject: [PATCH 1/2] fix:optimized data loading in locations.ts --- packages/app-store/locations.ts | 120 ++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 44 deletions(-) diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 8ff7fed9144072..bb8092b4edc236 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -5,7 +5,6 @@ import type { TFunction } from "i18next"; import { isValidPhoneNumber } from "libphonenumber-js"; import { z } from "zod"; -import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData"; import logger from "@calcom/lib/logger"; import { BookingStatus } from "@calcom/prisma/enums"; import type { Ensure, Optional } from "@calcom/types/utils"; @@ -231,62 +230,95 @@ export type BookingLocationValue = string; export const AppStoreLocationType: Record = {}; -const locationsFromApps: EventLocationTypeFromApp[] = []; - -for (const [appName, meta] of Object.entries(appStoreMetadata)) { - const location = meta.appData?.location; - if (location) { - // TODO: This template variable replacement should happen once during app-store:build. - for (const [key, value] of Object.entries(location)) { - if (typeof value === "string") { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - location[key] = value.replace(/{SLUG}/g, meta.slug).replace(/{TITLE}/g, meta.name); +// Lazy-loaded app locations +let locationsFromApps: EventLocationTypeFromApp[] | null = null; +let combinedLocations: EventLocationType[] | null = null; + +const loadAppLocations = (): EventLocationTypeFromApp[] => { + if (locationsFromApps !== null) { + return locationsFromApps; + } + + // Lazy import of app metadata - this will only load when first accessed + // Use dynamic import to avoid loading at startup + try { + // Since this is server-side and the module should be available, use require + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { appStoreMetadata } = require("@calcom/app-store/bookerAppsMetaData"); + processAppMetadata(appStoreMetadata); + } catch (error) { + console.warn("Failed to load app metadata, app locations may not be available", error); + locationsFromApps = []; + } + + return locationsFromApps; +}; + +const processAppMetadata = (appStoreMetadata: any) => { + locationsFromApps = []; + + for (const [appName, meta] of Object.entries(appStoreMetadata)) { + const location = meta.appData?.location; + if (location) { + // TODO: This template variable replacement should happen once during app-store:build. + for (const [key, value] of Object.entries(location)) { + if (typeof value === "string") { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + location[key] = value.replace(/{SLUG}/g, meta.slug).replace(/{TITLE}/g, meta.name); + } } - } - const newLocation = { - ...location, - messageForOrganizer: location.messageForOrganizer || `Set ${location.label} link`, - iconUrl: meta.logo, - // For All event location apps, locationLink is where we store the input - // TODO: locationLink and link seems redundant. We can modify the code to keep just one of them. - variable: location.variable || "locationLink", - defaultValueVariable: location.defaultValueVariable || "link", - }; - - // Static links always require organizer to input - if (newLocation.linkType === "static") { - newLocation.organizerInputType = location.organizerInputType || "text"; - if (newLocation.organizerInputPlaceholder?.match(/https?:\/\//)) { - // HACK: Translation ends up removing https? if it's in the beginning :( - newLocation.organizerInputPlaceholder = ` ${newLocation.organizerInputPlaceholder}`; + const newLocation = { + ...location, + messageForOrganizer: location.messageForOrganizer || `Set ${location.label} link`, + iconUrl: meta.logo, + // For All event location apps, locationLink is where we store the input + // TODO: locationLink and link seems redundant. We can modify the code to keep just one of them. + variable: location.variable || "locationLink", + defaultValueVariable: location.defaultValueVariable || "link", + }; + + // Static links always require organizer to input + if (newLocation.linkType === "static") { + newLocation.organizerInputType = location.organizerInputType || "text"; + if (newLocation.organizerInputPlaceholder?.match(/https?:\/\//)) { + // HACK: Translation ends up removing https? if it's in the beginning :( + newLocation.organizerInputPlaceholder = ` ${newLocation.organizerInputPlaceholder}`; + } + } else { + newLocation.organizerInputType = null; } - } else { - newLocation.organizerInputType = null; - } - AppStoreLocationType[appName] = newLocation.type; + AppStoreLocationType[appName] = newLocation.type; - locationsFromApps.push({ - ...newLocation, - }); + locationsFromApps.push({ + ...newLocation, + }); + } + } +}; + +const getCombinedLocations = (): EventLocationType[] => { + if (combinedLocations !== null) { + return combinedLocations; } -} -const locations = [...defaultLocations, ...locationsFromApps]; + const appLocations = loadAppLocations(); + combinedLocations = [...defaultLocations, ...appLocations]; + return combinedLocations; +}; export const getLocationFromApp = (locationType: string) => - locationsFromApps.find((l) => l.type === locationType); + loadAppLocations().find((l) => l.type === locationType); -// TODO: Rename this to getLocationByType() export const getEventLocationType = (locationType: string | undefined | null) => - locations.find((l) => l.type === locationType); + getCombinedLocations().find((l) => l.type === locationType); const getStaticLinkLocationByValue = (value: string | undefined | null) => { if (!value) { return null; } - return locations.find((l) => { + return getCombinedLocations().find((l) => { if (l.default || l.linkType == "dynamic" || !l.urlRegExp) { return; } @@ -489,14 +521,14 @@ export const getTranslatedLocation = ( export const getOrganizerInputLocationTypes = () => { const result: DefaultEventLocationType["type"] | EventLocationTypeFromApp["type"][] = []; - const organizerInputTypeLocations = locations.filter((location) => !!location.organizerInputType); + const organizerInputTypeLocations = getCombinedLocations().filter((location) => !!location.organizerInputType); organizerInputTypeLocations?.forEach((l) => result.push(l.type)); return result; }; export const isAttendeeInputRequired = (locationType: string) => { - const location = locations.find((l) => l.type === locationType); + const location = getCombinedLocations().find((l) => l.type === locationType); if (!location) { // Consider throwing an error here. This shouldn't happen normally. return false; From 0e2856bcc92012cd205163f9713516f182ae821d Mon Sep 17 00:00:00 2001 From: thetanav Date: Sat, 4 Oct 2025 20:09:00 +0530 Subject: [PATCH 2/2] app store metadata loading lazy --- .../utils/bookingScenario/bookingScenario.ts | 2 +- packages/app-store-cli/src/build.ts | 1 + packages/app-store/_appRegistry.ts | 21 +++++++--- packages/app-store/appStoreMetaData.ts | 41 +++++++++++++++---- .../app-store/getNormalizedAppMetadata.ts | 9 +--- packages/app-store/utils.ts | 36 +++++++++------- scripts/seed-app-store.ts | 1 - 7 files changed, 73 insertions(+), 38 deletions(-) diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 7577bddfd8a1cc..d74bc3ea0a1635 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -7,7 +7,7 @@ import { vi } from "vitest"; import "vitest-fetch-mock"; import type { z } from "zod"; -import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; +import { appStoreMetadata } from "@calcom/app-store/apps.metadata.generated"; import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook"; import { weekdayToWeekIndex, type WeekDays } from "@calcom/lib/dayjs"; import type { HttpError } from "@calcom/lib/http-error"; diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 06fb4e8511102c..80762f459e8487 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -255,6 +255,7 @@ function generateFiles() { importName: "metadata", }, ], + lazyImport: true, }) ); diff --git a/packages/app-store/_appRegistry.ts b/packages/app-store/_appRegistry.ts index 202045df336a07..8cfe9e92ff1040 100644 --- a/packages/app-store/_appRegistry.ts +++ b/packages/app-store/_appRegistry.ts @@ -1,4 +1,4 @@ -import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; +import { getAppMetadata } from "@calcom/app-store/appStoreMetaData"; import { getAppFromSlug } from "@calcom/app-store/utils"; import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp"; import { getAllDelegationCredentialsForUser } from "@calcom/lib/delegationCredential/server"; @@ -13,20 +13,29 @@ export type TDependencyData = { installed?: boolean; }[]; +// Use the centralized getAppMetadata function + /** * Get App metadata either using dirName or slug */ export async function getAppWithMetadata(app: { dirName: string } | { slug: string }) { let appMetadata: App | null; + let dirName: string; if ("dirName" in app) { - appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App; + dirName = app.dirName; + appMetadata = await getAppMetadata(dirName); } else { - const foundEntry = Object.entries(appStoreMetadata).find(([, meta]) => { - return meta.slug === app.slug; + // For slug-based lookup, query the database to get the dirName + const dbApp = await prisma.app.findUnique({ + where: { slug: app.slug }, + select: { dirName: true }, }); - if (!foundEntry) return null; - appMetadata = foundEntry[1] as App; + + if (!dbApp) return null; + + dirName = dbApp.dirName; + appMetadata = await getAppMetadata(dirName); } if (!appMetadata) return null; diff --git a/packages/app-store/appStoreMetaData.ts b/packages/app-store/appStoreMetaData.ts index 72502226eb0cfe..c83c5d6f6d3d1a 100644 --- a/packages/app-store/appStoreMetaData.ts +++ b/packages/app-store/appStoreMetaData.ts @@ -1,14 +1,39 @@ import type { AppMeta } from "@calcom/types/App"; -import { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated"; import { getNormalizedAppMetadata } from "./getNormalizedAppMetadata"; -type RawAppStoreMetaData = typeof rawAppStoreMetadata; -type AppStoreMetaData = { - [key in keyof RawAppStoreMetaData]: Omit & { dirName: string }; -}; +// Create a cache for loaded metadata +const metadataCache = new Map(); -export const appStoreMetadata = {} as AppStoreMetaData; -for (const [key, value] of Object.entries(rawAppStoreMetadata)) { - appStoreMetadata[key as keyof typeof appStoreMetadata] = getNormalizedAppMetadata(value); +// Async function to get metadata by dynamically importing +export async function getAppMetadata(dirName: string): Promise { + if (metadataCache.has(dirName)) { + return metadataCache.get(dirName)!; + } + + try { + // Try to import config.json first + const configModule = await import(`./${dirName}/config.json`); + const normalized = getNormalizedAppMetadata(configModule.default); + metadataCache.set(dirName, normalized); + return normalized; + } catch { + try { + // Fallback to _metadata.ts + const metadataModule = await import(`./${dirName}/_metadata`); + const normalized = getNormalizedAppMetadata(metadataModule.metadata); + metadataCache.set(dirName, normalized); + return normalized; + } catch { + return null; + } + } } + +// Keep the old synchronous interface for backward compatibility +// But make it throw an error to encourage migration to async +export const appStoreMetadata = new Proxy({} as any, { + get(target, prop: string) { + throw new Error(`appStoreMetadata is no longer synchronous. Use getAppMetadata('${prop}') instead.`); + }, +}); diff --git a/packages/app-store/getNormalizedAppMetadata.ts b/packages/app-store/getNormalizedAppMetadata.ts index de9c6ce6a72cac..d18c099eec7cd0 100644 --- a/packages/app-store/getNormalizedAppMetadata.ts +++ b/packages/app-store/getNormalizedAppMetadata.ts @@ -1,15 +1,8 @@ import type { AppMeta } from "@calcom/types/App"; -// We have to import all the booker-apps config/metadata in here as without that we couldn't -import type { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated"; import { getAppAssetFullPath } from "./getAppAssetFullPath"; -type RawAppStoreMetaData = typeof rawAppStoreMetadata; -type AppStoreMetaData = { - [key in keyof RawAppStoreMetaData]: AppMeta; -}; - -export const getNormalizedAppMetadata = (appMeta: RawAppStoreMetaData[keyof RawAppStoreMetaData]) => { +export const getNormalizedAppMetadata = (appMeta: Record) => { const dirName = "dirName" in appMeta ? appMeta.dirName : appMeta.slug; if (!dirName) { throw new Error(`Couldn't derive dirName for app ${appMeta.name}`); diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index 57f86594f96d2e..efffd0b50875aa 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -1,6 +1,6 @@ // If you import this file on any app it should produce circular dependency // import appStore from "./index"; -import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; +import { getAppMetadata } from "@calcom/app-store/appStoreMetaData"; import type { EventLocationType } from "@calcom/app-store/locations"; import logger from "@calcom/lib/logger"; import { getPiiFreeCredential } from "@calcom/lib/piiFreeData"; @@ -18,19 +18,22 @@ export type LocationOption = { disabled?: boolean; }; -const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => { - const metadata = appStoreMetadata[key as keyof typeof appStoreMetadata] as AppMeta; +// Cache for loaded apps +let ALL_APPS_CACHE: AppMeta[] | null = null; +let ALL_APPS_MAP_CACHE: Record | null = null; - store[key] = metadata; +// Function to load all apps metadata +async function loadAllApps() { + if (ALL_APPS_CACHE && ALL_APPS_MAP_CACHE) { + return { apps: ALL_APPS_CACHE, appsMap: ALL_APPS_MAP_CACHE }; + } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - delete store[key]["/*"]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - delete store[key]["__createdUsingCli"]; - return store; -}, {} as Record); + // For now, return empty arrays since we can't synchronously load all apps + // This needs to be changed to load apps from database or a static list + ALL_APPS_CACHE = []; + ALL_APPS_MAP_CACHE = {}; + return { apps: ALL_APPS_CACHE, appsMap: ALL_APPS_MAP_CACHE }; +} export type CredentialDataWithTeamName = CredentialForCalendarService & { team?: { @@ -38,7 +41,10 @@ export type CredentialDataWithTeamName = CredentialForCalendarService & { } | null; }; -export const ALL_APPS = Object.values(ALL_APPS_MAP); +// For backward compatibility, provide synchronous access +// But this will be empty until we implement proper loading +export const ALL_APPS: AppMeta[] = []; +const ALL_APPS_MAP: Record = {}; /** * This should get all available apps to the user based on his saved @@ -131,7 +137,9 @@ export function getAppType(name: string): string { } export function getAppFromSlug(slug: string | undefined): AppMeta | undefined { - return ALL_APPS.find((app) => app.slug === slug); + // For now, return undefined since we don't have preloaded data + // This needs to be implemented properly + return undefined; } export function getAppFromLocationValue(type: string): AppMeta | undefined { diff --git a/scripts/seed-app-store.ts b/scripts/seed-app-store.ts index 85df186b5ec25a..cd010b55d70089 100644 --- a/scripts/seed-app-store.ts +++ b/scripts/seed-app-store.ts @@ -5,7 +5,6 @@ import dotEnv from "dotenv"; import path from "path"; -import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import { AppCategories } from "@calcom/prisma/enums";