import type { ComponentType } from "react"; import { objectEntries } from "@homarr/common"; import type { WidgetKind } from "@homarr/definitions"; import type { SettingsContextProps } from "@homarr/settings/creator"; import type { WidgetComponentProps, WidgetDefinition } from "./definition"; import type { WidgetOptionDefinition } from "./options"; type WidgetModule = { definition: WidgetDefinition; componentLoader: () => Promise<{ default: ComponentType> }>; }; type WidgetComponentModule = { default: ComponentType>; }; export type WidgetResources = { definition: WidgetDefinition; Component: ComponentType>; }; const moduleLoaders: Record Promise> = { clock: () => import("./clock") as Promise, weather: () => import("./weather") as Promise, app: () => import("./app") as Promise, assistant: () => import("./assistant") as Promise, archiveTeamWarrior: () => import("./archive-team-warrior") as Promise, anchorNote: () => import("./anchor-note") as Promise, notebook: () => import("./notebook") as Promise, iframe: () => import("./iframe") as Promise, video: () => import("./video") as Promise, dnsHoleSummary: () => import("./dns-hole/summary") as Promise, dnsHoleControls: () => import("./dns-hole/controls") as Promise, "smartHome-entityState": () => import("./smart-home/entity-state") as Promise, "smartHome-executeAutomation": () => import("./smart-home/execute-automation") as Promise, stockPrice: () => import("./stocks") as Promise, mediaServer: () => import("./media-server") as Promise, calendar: () => import("./calendar") as Promise, downloads: () => import("./downloads") as Promise, "mediaRequests-requestList": () => import("./media-requests/list") as Promise, "mediaRequests-requestStats": () => import("./media-requests/stats") as Promise, mediaMissing: () => import("./media-missing") as Promise, networkControllerSummary: () => import("./network-controller/summary") as Promise, networkControllerStatus: () => import("./network-controller/network-status") as Promise, rssFeed: () => import("./rssFeed") as Promise, bookmarks: () => import("./bookmarks") as Promise, bazarr: () => import("./bazarr") as Promise, indexerManager: () => import("./indexer-manager") as Promise, healthMonitoring: () => import("./health-monitoring") as Promise, mediaTranscoding: () => import("./media-transcoding") as Promise, minecraftServerStatus: () => import("./minecraft/server-status") as Promise, dockerContainers: () => import("./docker") as Promise, releases: () => import("./releases") as Promise, firewall: () => import("./firewall") as Promise, notifications: () => import("./notifications") as Promise, mediaReleases: () => import("./media-releases") as Promise, systemResources: () => import("./system-resources") as Promise, coolify: () => import("./coolify") as Promise, systemDisks: () => import("./system-disks") as Promise, timetable: () => import("./timetable") as Promise, "immich-serverStats": () => import("./immich/server-stats") as Promise, "immich-albumCarousel": () => import("./immich/album-carousel") as Promise, paperlessNgx: () => import("./paperless-ngx") as Promise, patchmon: () => import("./patchmon") as Promise, tracearr: () => import("./tracearr") as Promise, speedtestTracker: () => import("./speedtest-tracker") as Promise, uptimeKuma: () => import("./uptime-kuma") as Promise, audioStats: () => import("./audio-stats") as Promise, umami: () => import("./umami") as Promise, vpn: () => import("./vpn") as Promise, ups: () => import("./ups") as Promise, beszelSystemTable: () => import("./beszel-system-table") as Promise, beszelSystemGrid: () => import("./beszel-system-grid") as Promise, beszelAlerts: () => import("./beszel-alerts") as Promise, beszelSystemStats: () => import("./beszel-system-stats") as Promise, traefik: () => import("./traefik") as Promise, customApi: () => import("./custom-api") as Promise, wud: () => import("./wud") as Promise, }; // Keep these imports explicit so Turbopack can create one discoverable chunk per // widget implementation. Loading this registry does not load the implementations. const componentLoaders: Record Promise> = { clock: () => import("./clock/component") as Promise, weather: () => import("./weather/component") as Promise, app: () => import("./app/component") as Promise, assistant: () => import("./assistant/component") as Promise, archiveTeamWarrior: () => import("./archive-team-warrior/component") as Promise, anchorNote: () => import("./anchor-note/component") as Promise, notebook: () => import("./notebook/component") as Promise, iframe: () => import("./iframe/component") as Promise, video: () => import("./video/component") as Promise, dnsHoleSummary: () => import("./dns-hole/summary/component") as Promise, dnsHoleControls: () => import("./dns-hole/controls/component") as Promise, "smartHome-entityState": () => import("./smart-home/entity-state/component") as Promise, "smartHome-executeAutomation": () => import("./smart-home/execute-automation/component") as Promise, stockPrice: () => import("./stocks/component") as Promise, mediaServer: () => import("./media-server/component") as Promise, calendar: () => import("./calendar/component") as Promise, downloads: () => import("./downloads/component") as Promise, "mediaRequests-requestList": () => import("./media-requests/list/component") as Promise, "mediaRequests-requestStats": () => import("./media-requests/stats/component") as Promise, mediaMissing: () => import("./media-missing/component") as Promise, networkControllerSummary: () => import("./network-controller/summary/component") as Promise, networkControllerStatus: () => import("./network-controller/network-status/component") as Promise, rssFeed: () => import("./rssFeed/component") as Promise, bookmarks: () => import("./bookmarks/component") as Promise, bazarr: () => import("./bazarr/component") as Promise, indexerManager: () => import("./indexer-manager/component") as Promise, healthMonitoring: () => import("./health-monitoring/component") as Promise, mediaTranscoding: () => import("./media-transcoding/component") as Promise, minecraftServerStatus: () => import("./minecraft/server-status/component") as Promise, dockerContainers: () => import("./docker/component") as Promise, releases: () => import("./releases/component") as Promise, firewall: () => import("./firewall/component") as Promise, notifications: () => import("./notifications/component") as Promise, mediaReleases: () => import("./media-releases/component") as Promise, systemResources: () => import("./system-resources/component") as Promise, coolify: () => import("./coolify/component") as Promise, systemDisks: () => import("./system-disks/component") as Promise, timetable: () => import("./timetable/component") as Promise, "immich-serverStats": () => import("./immich/server-stats/component") as Promise, "immich-albumCarousel": () => import("./immich/album-carousel/component") as Promise, paperlessNgx: () => import("./paperless-ngx/component") as Promise, patchmon: () => import("./patchmon/component") as Promise, tracearr: () => import("./tracearr/component") as Promise, speedtestTracker: () => import("./speedtest-tracker/component") as Promise, uptimeKuma: () => import("./uptime-kuma/component") as Promise, audioStats: () => import("./audio-stats/component") as Promise, umami: () => import("./umami/component") as Promise, vpn: () => import("./vpn/component") as Promise, ups: () => import("./ups/component") as Promise, beszelSystemTable: () => import("./beszel-system-table/component") as Promise, beszelSystemGrid: () => import("./beszel-system-grid/component") as Promise, beszelAlerts: () => import("./beszel-alerts/component") as Promise, beszelSystemStats: () => import("./beszel-system-stats/component") as Promise, traefik: () => import("./traefik/component") as Promise, customApi: () => import("./custom-api/component") as Promise, wud: () => import("./wud/component") as Promise, }; const definitionPromises = new Map>(); export const widgetKinds = Object.keys(moduleLoaders) as WidgetKind[]; export const createRetryableLoader = (loaders: ReadonlyMap Promise>) => { const promises = new Map>(); return (key: TKey) => { const existing = promises.get(key); if (existing) return existing; const loader = loaders.get(key); if (!loader) throw new Error(`No loader is registered for ${String(key)}`); const promise = loader(); promises.set(key, promise); void promise.catch(() => { if (promises.get(key) === promise) promises.delete(key); }); return promise; }; }; export const loadWidgetModule = createRetryableLoader( new Map(Object.entries(moduleLoaders) as [WidgetKind, () => Promise][]), ); export const loadWidgetComponent = createRetryableLoader( new Map(Object.entries(componentLoaders) as [WidgetKind, () => Promise][]), ); export const loadWidgetResources = createRetryableLoader( new Map( (Object.keys(moduleLoaders) as WidgetKind[]).map((kind) => [ kind, async () => { const [{ definition }, { default: Component }] = await Promise.all([ loadWidgetModule(kind), loadWidgetComponent(kind), ]); return { definition, Component } satisfies WidgetResources; }, ]), ), ); export const loadWidgetDefinition = (kind: WidgetKind) => { const existing = definitionPromises.get(kind); if (existing) return existing; const promise = loadWidgetModule(kind).then(({ definition }) => definition); definitionPromises.set(kind, promise); void promise.catch(() => { if (definitionPromises.get(kind) === promise) definitionPromises.delete(kind); }); return promise; }; let allWidgetDefinitionsPromise: Promise> | undefined; export const loadAllWidgetDefinitions = () => { if (allWidgetDefinitionsPromise) return allWidgetDefinitionsPromise; const promise = Promise.all(widgetKinds.map(async (kind) => [kind, await loadWidgetDefinition(kind)] as const)).then( (entries) => new Map(entries), ); allWidgetDefinitionsPromise = promise; void promise.catch(() => { if (allWidgetDefinitionsPromise === promise) allWidgetDefinitionsPromise = undefined; }); return promise; }; export const reduceWidgetOptionsWithDefinition = ( definition: WidgetDefinition, settings: Pick, currentValue: Record = {}, ) => { const options = definition.createOptions(settings) as Record; return objectEntries(options).reduce( (prev, [key, value]) => ({ ...prev, [key]: currentValue[key] ?? value.defaultValue, }), {} as Record, ); };