From 9c245e9c2350bb75cb1e2490597af7198a5e9020 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 11 Oct 2025 23:10:15 -0700 Subject: [PATCH] Add distribution detection pattern (#6028) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Establishes distribution-specific code pattern using compile-time constants and dead code elimination. Demonstrates with Help Center by hiding extension manager and update buttons in cloud distribution. Below commentary makes assumption that minifcation and tree-shaking is enabled (which isn't true yet, but will be eventually). ## Changes - **What**: Added `src/platform/distribution/types.ts` with distribution detection via `__DISTRIBUTION__` variable - **Build**: Vite replaces `__DISTRIBUTION__` at build time using environment variables - **Tree-shaking**: All code not relevant to target distribution is DCR'd and eliminated from bundle - **Example**: Help Center hides "Manager Extension" menu item and "Update" buttons in cloud builds ## Pattern This PR defines a `__DISTRIBUTION__` variable which gets replaced at build time by Vite using environment variables. All code not relevant to the given distribution is then DCR'd and tree-shaken. For simple cases (like this Help Center PR), import `isCloud` and use compile-time conditionals: ```typescript import { isCloud } from '@/platform/distribution/types' if (!isCloud) { items.push({ key: 'manager', action: async () => { await useManagerState().openManager({ ... }) } }) } ``` The code is DCR'd at build time so there's zero runtime overhead - we don't even incur the `if (isCloud)` cost because Terser eliminates it. For complex services later, we'll add interfaces and use an index.ts that exports different implementations under the same alias per distribution. It will resemble a DI container but simpler since we don't need runtime discovery like backend devs do. This guarantees types and makes testing easier. Example for services: ```typescript // src/platform/storage/index.ts import { isCloud } from '@/platform/distribution/types' if (isCloud) { export { CloudStorage as StorageService } from './cloud' } else { export { LocalStorage as StorageService } from './local' } ``` Example for component variants: ```typescript // src/components/downloads/index.ts import { isCloud } from '@/platform/distribution/types' if (isCloud) { export { default as DownloadButton } from './DownloadButton.cloud.vue' } else { export { default as DownloadButton } from './DownloadButton.desktop.vue' } ``` ## Implementation Details Distribution types (`src/platform/distribution/types.ts`): ```typescript type Distribution = 'desktop' | 'localhost' | 'cloud' declare global { const __DISTRIBUTION__: Distribution } const DISTRIBUTION: Distribution = __DISTRIBUTION__ export const isCloud = DISTRIBUTION === 'cloud' ``` Vite configuration adds the define: ```typescript const DISTRIBUTION = (process.env.DISTRIBUTION || 'localhost') as | 'desktop' | 'localhost' | 'cloud' export default defineConfig({ define: { __DISTRIBUTION__: JSON.stringify(DISTRIBUTION) } }) ``` ## Build Commands ```bash pnpm build # localhost (default) DISTRIBUTION=cloud pnpm build # cloud DISTRIBUTION=desktop pnpm build # desktop ``` ## Future Applications This pattern can be used with auth or telemetry services - which will guarantee all the telemetry code, for example, is not even in the code distributed in OSS Comfy whatsoever while still being able to develop off `main`. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6028-Add-distribution-detection-pattern-28a6d73d365081b08767d395472cd1bc) by [Unito](https://www.unito.io) --- .../helpcenter/HelpCenterMenuContent.vue | 39 ++++++++++++------- src/platform/distribution/types.ts | 18 +++++++++ tsconfig.json | 1 + vite.config.mts | 8 +++- 4 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 src/platform/distribution/types.ts diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue index a58cb4a76..bfa1cbc96 100644 --- a/src/components/helpcenter/HelpCenterMenuContent.vue +++ b/src/components/helpcenter/HelpCenterMenuContent.vue @@ -135,6 +135,7 @@ import type { CSSProperties, Component } from 'vue' import { useI18n } from 'vue-i18n' import PuzzleIcon from '@/components/icons/PuzzleIcon.vue' +import { isCloud } from '@/platform/distribution/types' import { useSettingStore } from '@/platform/settings/settingStore' import type { ReleaseNote } from '@/platform/updates/common/releaseService' import { useReleaseStore } from '@/platform/updates/common/releaseStore' @@ -265,7 +266,7 @@ const moreMenuItem = computed(() => ) const menuItems = computed(() => { - return [ + const items: MenuItem[] = [ { key: 'docs', type: 'item', @@ -305,8 +306,12 @@ const menuItems = computed(() => { void commandStore.execute('Comfy.ContactSupport') emit('close') } - }, - { + } + ] + + // Extension manager - only in non-cloud distributions + if (!isCloud) { + items.push({ key: 'manager', type: 'item', icon: PuzzleIcon, @@ -319,17 +324,20 @@ const menuItems = computed(() => { }) emit('close') } - }, - { - key: 'more', - type: 'item', - icon: '', - label: t('helpCenter.more'), - visible: hasVisibleMoreItems.value, - action: () => {}, // No action for more item - items: moreItems.value - } - ] + }) + } + + items.push({ + key: 'more', + type: 'item', + icon: '', + label: t('helpCenter.more'), + visible: hasVisibleMoreItems.value, + action: () => {}, // No action for more item + items: moreItems.value + }) + + return items }) // Utility Functions @@ -420,6 +428,9 @@ const formatReleaseDate = (dateString?: string): string => { } const shouldShowUpdateButton = (release: ReleaseNote): boolean => { + // Hide update buttons in cloud distribution + if (isCloud) return false + return ( releaseStore.shouldShowUpdateButton && release === releaseStore.recentReleases[0] diff --git a/src/platform/distribution/types.ts b/src/platform/distribution/types.ts new file mode 100644 index 000000000..8da710973 --- /dev/null +++ b/src/platform/distribution/types.ts @@ -0,0 +1,18 @@ +/** + * Distribution types and compile-time constants for managing + * multi-distribution builds (Desktop, Localhost, Cloud) + */ + +type Distribution = 'desktop' | 'localhost' | 'cloud' + +declare global { + const __DISTRIBUTION__: Distribution +} + +/** Current distribution - replaced at compile time */ +const DISTRIBUTION: Distribution = __DISTRIBUTION__ + +/** Distribution type checks */ +// const isDesktop = DISTRIBUTION === 'desktop' +// const isLocalhost = DISTRIBUTION === 'localhost' +export const isCloud = DISTRIBUTION === 'cloud' diff --git a/tsconfig.json b/tsconfig.json index e98c48c4d..ba0d3de22 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -58,6 +58,7 @@ "src/types/**/*.d.ts", "tailwind.config.ts", "tests-ui/**/*", + "vite.config.mts", "vitest.config.ts", // "vitest.setup.ts", ] diff --git a/vite.config.mts b/vite.config.mts index 32de5f6ed..c68f386f1 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -24,6 +24,11 @@ const DISABLE_VUE_PLUGINS = process.env.DISABLE_VUE_PLUGINS === 'true' const DEV_SERVER_COMFYUI_URL = process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188' +const DISTRIBUTION = (process.env.DISTRIBUTION || 'localhost') as + | 'desktop' + | 'localhost' + | 'cloud' + export default defineConfig({ base: '', server: { @@ -195,7 +200,8 @@ export default defineConfig({ __SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || ''), __ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''), __ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || ''), - __USE_PROD_CONFIG__: process.env.USE_PROD_CONFIG === 'true' + __USE_PROD_CONFIG__: process.env.USE_PROD_CONFIG === 'true', + __DISTRIBUTION__: JSON.stringify(DISTRIBUTION) }, resolve: {