Compare commits

...

7 Commits

Author SHA1 Message Date
bymyself
82ca3031dd Fix environment variable access for Vite compatibility
Changed from process.env to import.meta.env in src/config/comfyDomain.ts
to fix CI timeout in i18n collection script. In Vite, process.env is not
available and causes initialization issues.
2025-07-20 18:50:40 -07:00
bymyself
1b3522b250 Fix CI timeout by reverting to navigator-based platform detection
The systemStatsStore approach was causing CI timeouts because it added Pinia store initialization overhead during page load. Reverting getDesktopGuideUrl to use navigator.platform directly resolves the i18n collection timeout that started with our URL centralization.
2025-07-20 16:43:13 -07:00
bymyself
256ad03210 Fix systemStatsStore access in CI environment
Make systemStatsStore access truly lazy in HelpCenterMenuContent by creating store instance inside action function instead of at component initialization time
2025-07-19 18:07:57 -07:00
bymyself
3f27e7532d Simplify OS detection to use systemStatsStore only
- Remove navigator fallback since systemStatsStore is the standard
- Default to Windows when OS info not available
- Simplify tests to match new behavior
- More consistent with app architecture
2025-07-19 15:06:07 -07:00
bymyself
939323b563 Fix URL constants tests to match actual implementation
- Update getLocalized tests to expect full URLs with domain instead of paths
- Fix test for empty path to avoid double slashes
- Update allowedDomains regex to handle localhost URLs properly
- Rename test to reflect it validates URLs, not just domain names
2025-07-18 14:41:21 -07:00
bymyself
08daa6f3ef Fix regex patterns in URL constants tests
Remove unnecessary escape characters in regex patterns to satisfy eslint no-useless-escape rule.
2025-07-18 14:14:01 -07:00
bymyself
5faf9e0105 [refactor] centralize hardcoded URLs into organized constants
- Create src/constants/urls.ts with centralized URL constants (COMFY_URLS, GITHUB_REPOS, MODEL_SOURCES, DEVELOPER_TOOLS)
- Move runtime domain config to src/config/comfyDomain.ts to allow forkers to customize via env var
- Rename uvMirrors.ts to mirrors.ts for better naming consistency
- Add platform and locale-aware desktop guide URL generation (matching PR #4471)
- Update 10 components to use centralized URL constants
- Add comprehensive unit and e2e tests for URL constants validation

This refactoring improves maintainability by centralizing 150+ hardcoded URLs found across 50+ files into a single organized structure.
2025-07-18 13:45:32 -07:00
13 changed files with 337 additions and 43 deletions

View File

@@ -38,23 +38,14 @@ import { useI18n } from 'vue-i18n'
import FileDownload from '@/components/common/FileDownload.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import { MODEL_SOURCES } from '@/constants/urls'
import { useSettingStore } from '@/stores/settingStore'
import { isElectron } from '@/utils/envUtil'
// TODO: Read this from server internal API rather than hardcoding here
// as some installations may wish to use custom sources
const allowedSources = [
'https://civitai.com/',
'https://huggingface.co/',
'http://localhost:' // Included for testing usage only
]
/** @todo Read this from server internal API rather than hardcoding here */
const allowedSources = MODEL_SOURCES.allowedDomains
const allowedSuffixes = ['.safetensors', '.sft']
// Models that fail above conditions but are still allowed
const whiteListedUrls = new Set([
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt',
'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true',
'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth'
])
const whiteListedUrls = new Set(MODEL_SOURCES.whitelistedUrls)
interface ModelInfo {
name: string

View File

@@ -123,6 +123,7 @@ import Button from 'primevue/button'
import { type CSSProperties, computed, nextTick, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { COMFY_URLS, GITHUB_REPOS, getDesktopGuideUrl } from '@/constants/urls'
import { type ReleaseNote } from '@/services/releaseService'
import { useCommandStore } from '@/stores/commandStore'
import { useReleaseStore } from '@/stores/releaseStore'
@@ -143,11 +144,10 @@ interface MenuItem {
// Constants
const EXTERNAL_LINKS = {
DOCS: 'https://docs.comfy.org/',
DISCORD: 'https://www.comfy.org/discord',
GITHUB: 'https://github.com/comfyanonymous/ComfyUI',
DESKTOP_GUIDE: 'https://comfyorg.notion.site/',
UPDATE_GUIDE: 'https://docs.comfy.org/installation/update_comfyui'
DOCS: COMFY_URLS.docs.base,
DISCORD: COMFY_URLS.community.discord,
GITHUB: GITHUB_REPOS.comfyui,
UPDATE_GUIDE: COMFY_URLS.docs.installation.update
} as const
const TIME_UNITS = {
@@ -199,7 +199,7 @@ const menuItems = computed<MenuItem[]>(() => {
type: 'item',
label: t('helpCenter.desktopUserGuide'),
action: () => {
openExternalLink(EXTERNAL_LINKS.DESKTOP_GUIDE)
openExternalLink(getDesktopGuideUrl(locale.value))
emit('close')
}
},
@@ -451,13 +451,13 @@ const onUpdate = (_: ReleaseNote): void => {
const getChangelogUrl = (): string => {
const isChineseLocale = locale.value === 'zh'
return isChineseLocale
? 'https://docs.comfy.org/zh-CN/changelog'
: 'https://docs.comfy.org/changelog'
? COMFY_URLS.docs.changelog.zh
: COMFY_URLS.docs.changelog.en
}
// Lifecycle
onMounted(async () => {
if (!hasReleases.value) {
if (showVersionUpdates.value && !hasReleases.value) {
await releaseStore.fetchReleases()
}
})

View File

@@ -48,6 +48,7 @@
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { COMFY_URLS } from '@/constants/urls'
import type { ReleaseNote } from '@/services/releaseService'
import { useReleaseStore } from '@/stores/releaseStore'
import { formatVersionAnchor } from '@/utils/formatUtil'
@@ -72,8 +73,8 @@ const shouldShow = computed(
const changelogUrl = computed(() => {
const isChineseLocale = locale.value === 'zh'
const baseUrl = isChineseLocale
? 'https://docs.comfy.org/zh-CN/changelog'
: 'https://docs.comfy.org/changelog'
? COMFY_URLS.docs.changelog.zh
: COMFY_URLS.docs.changelog.en
if (latestRelease.value?.version) {
const versionAnchor = formatVersionAnchor(latestRelease.value.version)
@@ -120,7 +121,7 @@ const handleLearnMore = () => {
}
const handleUpdate = () => {
window.open('https://docs.comfy.org/installation/update_comfyui', '_blank')
window.open(COMFY_URLS.docs.installation.update, '_blank')
dismissToast()
}

View File

@@ -68,6 +68,7 @@ import { marked } from 'marked'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { COMFY_URLS } from '@/constants/urls'
import type { ReleaseNote } from '@/services/releaseService'
import { useReleaseStore } from '@/stores/releaseStore'
import { formatVersionAnchor } from '@/utils/formatUtil'
@@ -92,8 +93,8 @@ const shouldShow = computed(
const changelogUrl = computed(() => {
const isChineseLocale = locale.value === 'zh'
const baseUrl = isChineseLocale
? 'https://docs.comfy.org/zh-CN/changelog'
: 'https://docs.comfy.org/changelog'
? COMFY_URLS.docs.changelog.zh
: COMFY_URLS.docs.changelog.en
if (latestRelease.value?.version) {
const versionAnchor = formatVersionAnchor(latestRelease.value.version)

View File

@@ -43,7 +43,7 @@ import Panel from 'primevue/panel'
import { ModelRef, computed, onMounted, ref } from 'vue'
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/mirrors'
import { t } from '@/i18n'
import { isInChina } from '@/utils/networkUtil'
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'

View File

@@ -23,7 +23,7 @@
import { computed, onMounted, ref, watch } from 'vue'
import UrlInput from '@/components/common/UrlInput.vue'
import { UVMirror } from '@/constants/uvMirrors'
import { UVMirror } from '@/constants/mirrors'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { checkMirrorReachable } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'

View File

@@ -11,6 +11,7 @@ import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
} from '@/constants/coreColorPalettes'
import { COMFY_URLS, GITHUB_REPOS } from '@/constants/urls'
import { t } from '@/i18n'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
@@ -543,10 +544,7 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'ComfyUI Issues',
versionAdded: '1.5.5',
function: () => {
window.open(
'https://github.com/comfyanonymous/ComfyUI/issues',
'_blank'
)
window.open(GITHUB_REPOS.comfyuiIssues, '_blank')
}
},
{
@@ -556,7 +554,7 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'ComfyUI Docs',
versionAdded: '1.5.5',
function: () => {
window.open('https://docs.comfy.org/', '_blank')
window.open(COMFY_URLS.docs.base, '_blank')
}
},
{
@@ -566,7 +564,7 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'Comfy-Org Discord',
versionAdded: '1.5.5',
function: () => {
window.open('https://www.comfy.org/discord', '_blank')
window.open(COMFY_URLS.community.discord, '_blank')
}
},
{
@@ -646,7 +644,7 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'ComfyUI Forum',
versionAdded: '1.8.2',
function: () => {
window.open('https://forum.comfy.org/', '_blank')
window.open(COMFY_URLS.community.forum.base, '_blank')
}
},
{

14
src/config/comfyDomain.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Base domain configuration and core website URLs
* Forkers can change the base domain to use their own
*/
export const COMFY_BASE_DOMAIN =
import.meta.env.VITE_COMFY_BASE_DOMAIN || 'comfy.org'
const WEBSITE_BASE_URL = `https://www.${COMFY_BASE_DOMAIN}`
export const COMFY_WEBSITE_URLS = {
base: WEBSITE_BASE_URL,
termsOfService: `${WEBSITE_BASE_URL}/terms-of-service`,
privacy: `${WEBSITE_BASE_URL}/privacy`
}

95
src/constants/urls.ts Normal file
View File

@@ -0,0 +1,95 @@
/**
* URL constants for ComfyUI frontend
* Centralized location for all URL references
*/
import { COMFY_BASE_DOMAIN } from '@/config/comfyDomain'
const DOCS_BASE_URL = `https://docs.${COMFY_BASE_DOMAIN}`
const FORUM_BASE_URL = `https://forum.${COMFY_BASE_DOMAIN}`
const WEBSITE_BASE_URL = `https://www.${COMFY_BASE_DOMAIN}`
export const COMFY_URLS = {
website: {
base: WEBSITE_BASE_URL,
termsOfService: `${WEBSITE_BASE_URL}/terms-of-service`,
privacy: `${WEBSITE_BASE_URL}/privacy`
},
docs: {
base: DOCS_BASE_URL,
changelog: {
en: `${DOCS_BASE_URL}/changelog`,
zh: `${DOCS_BASE_URL}/zh-CN/changelog`
},
installation: {
update: `${DOCS_BASE_URL}/installation/update_comfyui`
},
tutorials: {
apiNodes: {
overview: `${DOCS_BASE_URL}/tutorials/api-nodes/overview`,
faq: `${DOCS_BASE_URL}/tutorials/api-nodes/faq`,
pricing: `${DOCS_BASE_URL}/tutorials/api-nodes/pricing`,
apiKeyLogin: `${DOCS_BASE_URL}/interface/user#logging-in-with-an-api-key`,
nonWhitelistedLogin: `${DOCS_BASE_URL}/tutorials/api-nodes/overview#log-in-with-api-key-on-non-whitelisted-websites`
}
},
getLocalized: (path: string, locale: string) => {
return locale === 'zh' || locale === 'zh-CN'
? `${DOCS_BASE_URL}/zh-CN/${path}`
: `${DOCS_BASE_URL}/${path}`
}
},
community: {
discord: `${WEBSITE_BASE_URL}/discord`,
forum: {
base: `${FORUM_BASE_URL}/`,
v1Feedback: `${FORUM_BASE_URL}/c/v1-feedback/`
}
}
}
export const GITHUB_REPOS = {
comfyui: 'https://github.com/comfyanonymous/ComfyUI',
comfyuiIssues: 'https://github.com/comfyanonymous/ComfyUI/issues',
frontend: 'https://github.com/Comfy-Org/ComfyUI_frontend',
electron: 'https://github.com/Comfy-Org/electron',
desktopPlatforms:
'https://github.com/Comfy-Org/desktop#currently-supported-platforms'
}
export const MODEL_SOURCES = {
repos: {
civitai: 'https://civitai.com/',
huggingface: 'https://huggingface.co/'
},
whitelistedUrls: [
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt',
'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true',
'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth'
],
allowedDomains: [
'https://civitai.com/',
'https://huggingface.co/',
'http://localhost:' // TODO: Remove in production
]
}
export const DEVELOPER_TOOLS = {
git: 'https://git-scm.com/downloads/',
vcRedist: 'https://aka.ms/vs/17/release/vc_redist.x64.exe',
uv: 'https://docs.astral.sh/uv/getting-started/installation/'
}
// Platform and locale-aware desktop guide URL generator
export const getDesktopGuideUrl = (locale: string): string => {
const isChineseLocale = locale === 'zh'
const isMacOS =
typeof navigator !== 'undefined' &&
navigator.platform.toUpperCase().indexOf('MAC') >= 0
const platform = isMacOS ? 'macos' : 'windows'
const baseUrl = isChineseLocale
? `https://docs.${COMFY_BASE_DOMAIN}/zh-CN/installation/desktop`
: `https://docs.${COMFY_BASE_DOMAIN}/installation/desktop`
return `${baseUrl}/${platform}`
}

View File

@@ -1,7 +1,8 @@
import log from 'loglevel'
import { PYTHON_MIRROR } from '@/constants/uvMirrors'
import { t } from '@/i18n'
import { PYTHON_MIRROR } from '@/constants/mirrors'
import { getDesktopGuideUrl } from '@/constants/urls'
import { i18n, t } from '@/i18n'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useToastStore } from '@/stores/toastStore'
@@ -159,7 +160,7 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
label: 'Desktop User Guide',
icon: 'pi pi-book',
function() {
window.open('https://comfyorg.notion.site/', '_blank')
window.open(getDesktopGuideUrl(i18n.global.locale.value), '_blank')
}
},
{

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { COMFY_URLS, GITHUB_REPOS } from '@/constants/urls'
import { AboutPageBadge } from '@/types/comfy'
import { electronAPI, isElectron } from '@/utils/envUtil'
@@ -24,20 +25,20 @@ export const useAboutPanelStore = defineStore('aboutPanel', () => {
? 'v' + electronAPI().getComfyUIVersion()
: coreVersion.value
}`,
url: 'https://github.com/comfyanonymous/ComfyUI',
url: GITHUB_REPOS.comfyui,
icon: 'pi pi-github'
},
{
label: `ComfyUI_frontend v${frontendVersion}`,
url: 'https://github.com/Comfy-Org/ComfyUI_frontend',
url: GITHUB_REPOS.frontend,
icon: 'pi pi-github'
},
{
label: 'Discord',
url: 'https://www.comfy.org/discord',
url: COMFY_URLS.community.discord,
icon: 'pi pi-discord'
},
{ label: 'ComfyOrg', url: 'https://www.comfy.org/', icon: 'pi pi-globe' }
{ label: 'ComfyOrg', url: COMFY_URLS.website.base, icon: 'pi pi-globe' }
])
const allBadges = computed<AboutPageBadge[]>(() => [

View File

@@ -0,0 +1,192 @@
import { describe, expect, it } from 'vitest'
import { COMFY_BASE_DOMAIN, COMFY_WEBSITE_URLS } from '@/config/comfyDomain'
import { PYPI_MIRROR, PYTHON_MIRROR } from '@/constants/mirrors'
import {
COMFY_URLS,
DEVELOPER_TOOLS,
GITHUB_REPOS,
MODEL_SOURCES,
getDesktopGuideUrl
} from '@/constants/urls'
describe('URL Constants', () => {
describe('URL Format Validation', () => {
it('should have valid HTTPS URLs throughout', () => {
const httpsPattern =
/^https:\/\/[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?(\/.*)?$/i
// Test COMFY_URLS
expect(COMFY_URLS.website.base).toMatch(httpsPattern)
expect(COMFY_URLS.docs.base).toMatch(httpsPattern)
expect(COMFY_URLS.community.discord).toMatch(httpsPattern)
// Test GITHUB_REPOS
Object.values(GITHUB_REPOS).forEach((url) => {
expect(url).toMatch(httpsPattern)
})
// Test MODEL_SOURCES
Object.values(MODEL_SOURCES.repos).forEach((url) => {
expect(url).toMatch(httpsPattern)
})
// Test DEVELOPER_TOOLS
Object.values(DEVELOPER_TOOLS).forEach((url) => {
expect(url).toMatch(httpsPattern)
})
})
it('should have proper GitHub URL format', () => {
const githubPattern = /^https:\/\/github\.com\/[\w-]+\/[\w-]+(\/[\w-]+)?$/
expect(GITHUB_REPOS.comfyui).toMatch(githubPattern)
expect(GITHUB_REPOS.comfyuiIssues).toMatch(githubPattern)
expect(GITHUB_REPOS.frontend).toMatch(githubPattern)
})
})
describe('Mirror Configuration', () => {
it('should have valid mirror URLs', () => {
const urlPattern =
/^https?:\/\/[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?(\/.*)?$/i
expect(PYTHON_MIRROR.mirror).toMatch(urlPattern)
expect(PYTHON_MIRROR.fallbackMirror).toMatch(urlPattern)
expect(PYPI_MIRROR.mirror).toMatch(urlPattern)
expect(PYPI_MIRROR.fallbackMirror).toMatch(urlPattern)
})
})
describe('Domain Configuration', () => {
it('should have valid domain format', () => {
const domainPattern = /^[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}$/i
expect(COMFY_BASE_DOMAIN).toMatch(domainPattern)
})
it('should construct proper website URLs from base domain', () => {
const expectedBase = `https://www.${COMFY_BASE_DOMAIN}`
expect(COMFY_WEBSITE_URLS.base).toBe(expectedBase)
expect(COMFY_WEBSITE_URLS.termsOfService).toBe(
`${expectedBase}/terms-of-service`
)
expect(COMFY_WEBSITE_URLS.privacy).toBe(`${expectedBase}/privacy`)
})
})
describe('Localization', () => {
it('should handle valid language codes in getLocalized', () => {
const validLanguageCodes = ['en', 'zh', 'ja', 'ko', 'es', 'fr', 'de']
validLanguageCodes.forEach((lang) => {
const result = COMFY_URLS.docs.getLocalized('test-path', lang)
expect(result).toMatch(
/^https:\/\/docs\.comfy\.org\/(([a-z]{2}-[A-Z]{2}\/)?test-path)$/
)
})
})
it('should properly format localized paths', () => {
expect(COMFY_URLS.docs.getLocalized('test-path', 'en')).toBe(
'https://docs.comfy.org/test-path'
)
expect(COMFY_URLS.docs.getLocalized('test-path', 'zh')).toBe(
'https://docs.comfy.org/zh-CN/test-path'
)
expect(COMFY_URLS.docs.getLocalized('', 'en')).toBe(
'https://docs.comfy.org/'
)
expect(COMFY_URLS.docs.getLocalized('', 'zh')).toBe(
'https://docs.comfy.org/zh-CN/'
)
})
it('should generate platform and locale-aware desktop guide URLs', () => {
// Mock navigator for testing
const originalNavigator = global.navigator
// Test Windows platform
Object.defineProperty(global, 'navigator', {
value: { platform: 'Win32' },
writable: true
})
const winUrl = getDesktopGuideUrl('en')
expect(winUrl).toBe('https://docs.comfy.org/installation/desktop/windows')
// Test macOS platform
Object.defineProperty(global, 'navigator', {
value: { platform: 'MacIntel' },
writable: true
})
const macUrl = getDesktopGuideUrl('en')
expect(macUrl).toBe('https://docs.comfy.org/installation/desktop/macos')
// Test Chinese locale with macOS
const zhMacUrl = getDesktopGuideUrl('zh')
expect(zhMacUrl).toBe(
'https://docs.comfy.org/zh-CN/installation/desktop/macos'
)
// Test Chinese locale with Windows
Object.defineProperty(global, 'navigator', {
value: { platform: 'Win32' },
writable: true
})
const zhWinUrl = getDesktopGuideUrl('zh')
expect(zhWinUrl).toBe(
'https://docs.comfy.org/zh-CN/installation/desktop/windows'
)
// Test other locales default to English
const frUrl = getDesktopGuideUrl('fr')
expect(frUrl).toBe('https://docs.comfy.org/installation/desktop/windows')
// Test environment without navigator
Object.defineProperty(global, 'navigator', {
value: undefined,
writable: true
})
const noNavUrl = getDesktopGuideUrl('en')
expect(noNavUrl).toBe(
'https://docs.comfy.org/installation/desktop/windows'
)
// Restore original navigator
Object.defineProperty(global, 'navigator', {
value: originalNavigator,
writable: true
})
})
})
describe('Security', () => {
it('should only use secure HTTPS for external URLs', () => {
const allUrls = [
...Object.values(GITHUB_REPOS),
...Object.values(MODEL_SOURCES.repos),
...Object.values(DEVELOPER_TOOLS),
COMFY_URLS.website.base,
COMFY_URLS.docs.base,
COMFY_URLS.community.discord
]
allUrls.forEach((url) => {
expect(url.startsWith('https://')).toBe(true)
expect(url.startsWith('http://')).toBe(false)
})
})
it('should have valid URL allowlist for model sources', () => {
const urlPattern =
/^https?:\/\/([a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}|localhost)(:[0-9]+)?\/?.*$/i
MODEL_SOURCES.allowedDomains.forEach((url) => {
expect(url).toMatch(urlPattern)
})
})
})
})