Files
ComfyUI_frontend/vite.config.mts
Alexander Brown b1dfbfaa09 chore: Replace prettier with oxfmt (#8177)
Configure oxfmt ignorePatterns to exclude non-JS/TS files (md, json,
css, yaml, etc.) to match previous Prettier behavior.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8177-chore-configure-oxfmt-to-format-only-JS-TS-Vue-files-2ee6d73d3650815080f3cc8a4a932109)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-01-20 16:44:08 -08:00

554 lines
16 KiB
TypeScript

import { sentryVitePlugin } from '@sentry/vite-plugin'
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
import { config as dotenvConfig } from 'dotenv'
import type { IncomingMessage, ServerResponse } from 'http'
import { Readable } from 'stream'
import type { ReadableStream as NodeReadableStream } from 'stream/web'
import { visualizer } from 'rollup-plugin-visualizer'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import typegpuPlugin from 'unplugin-typegpu/vite'
import { defineConfig } from 'vitest/config'
import type { ProxyOptions } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
import vueDevTools from 'vite-plugin-vue-devtools'
import { comfyAPIPlugin } from './build/plugins'
dotenvConfig()
const IS_DEV = process.env.NODE_ENV === 'development'
const SHOULD_MINIFY = process.env.ENABLE_MINIFY === 'true'
const ANALYZE_BUNDLE = process.env.ANALYZE_BUNDLE === 'true'
// vite dev server will listen on all addresses, including LAN and public addresses
const VITE_REMOTE_DEV = process.env.VITE_REMOTE_DEV === 'true'
const DISABLE_TEMPLATES_PROXY = process.env.DISABLE_TEMPLATES_PROXY === 'true'
const GENERATE_SOURCEMAP = process.env.GENERATE_SOURCEMAP !== 'false'
const IS_STORYBOOK = process.env.npm_lifecycle_event === 'storybook'
// Open Graph / Twitter Meta Tags Constants
const VITE_OG_URL = 'https://cloud.comfy.org'
const VITE_OG_TITLE =
'Comfy Cloud: Run ComfyUI online | Zero Setup, Powerful GPUs, Create anywhere'
const VITE_OG_DESC =
'Bring your creative ideas to life with Comfy Cloud. Build and run your workflows to generate stunning images and videos instantly using powerful GPUs — all from your browser, no installation required.'
const VITE_OG_IMAGE = `${VITE_OG_URL}/assets/images/og-image.png`
const VITE_OG_KEYWORDS = 'ComfyUI, Comfy Cloud, ComfyUI online'
// Auto-detect cloud mode from DEV_SERVER_COMFYUI_URL
const DEV_SERVER_COMFYUI_ENV_URL = process.env.DEV_SERVER_COMFYUI_URL
const IS_CLOUD_URL = DEV_SERVER_COMFYUI_ENV_URL?.includes('.comfy.org')
const DISTRIBUTION: 'desktop' | 'localhost' | 'cloud' =
process.env.DISTRIBUTION === 'desktop' ||
process.env.DISTRIBUTION === 'localhost' ||
process.env.DISTRIBUTION === 'cloud'
? process.env.DISTRIBUTION
: IS_CLOUD_URL
? 'cloud'
: 'localhost'
// Nightly builds are from main branch; RC/stable builds are from core/* branches
// Can be overridden via IS_NIGHTLY env var for testing
const IS_NIGHTLY =
process.env.IS_NIGHTLY === 'true' ||
(process.env.IS_NIGHTLY !== 'false' &&
process.env.CI === 'true' &&
process.env.GITHUB_REF_NAME === 'main')
// Disable Vue DevTools for production cloud distribution
const DISABLE_VUE_PLUGINS =
process.env.DISABLE_VUE_PLUGINS === 'true' ||
(DISTRIBUTION === 'cloud' && !IS_DEV) ||
IS_STORYBOOK
const DEV_SEVER_FALLBACK_URL =
DISTRIBUTION === 'cloud'
? 'https://stagingcloud.comfy.org'
: 'http://127.0.0.1:8188'
const DEV_SERVER_COMFYUI_URL =
DEV_SERVER_COMFYUI_ENV_URL || DEV_SEVER_FALLBACK_URL
const cloudProxyConfig =
DISTRIBUTION === 'cloud' ? { secure: false, changeOrigin: true } : {}
function handleGcsRedirect(
proxyRes: IncomingMessage,
_req: IncomingMessage,
res: ServerResponse
) {
const location = proxyRes.headers.location
const isGcsRedirect =
proxyRes.statusCode === 302 &&
location?.includes('storage.googleapis.com') &&
proxyRes.headers.via?.includes('google')
// Not a GCS redirect - pass through normally
if (!isGcsRedirect || !location) {
Object.keys(proxyRes.headers).forEach((key) => {
const value = proxyRes.headers[key]
if (value !== undefined) {
res.setHeader(key, value)
}
})
res.writeHead(proxyRes.statusCode || 200)
proxyRes.pipe(res)
return
}
// GCS redirect detected - fetch server-side to avoid CORS
fetch(location)
.then(async (gcsResponse) => {
if (!gcsResponse.body) {
res.statusCode = 500
res.end('Empty response from GCS')
return
}
// Set response headers from GCS
res.statusCode = 200
res.setHeader(
'Content-Type',
gcsResponse.headers.get('content-type') || 'application/octet-stream'
)
const contentLength = gcsResponse.headers.get('content-length')
if (contentLength) {
res.setHeader('Content-Length', contentLength)
}
// Convert Web ReadableStream to Node.js stream and pipe to client
const readable = Readable.fromWeb(gcsResponse.body as NodeReadableStream)
readable.pipe(res)
})
.catch((error) => {
console.error('Error fetching from GCS:', error)
res.statusCode = 500
res.end('Error fetching media')
})
}
const gcsRedirectProxyConfig: ProxyOptions = {
target: DEV_SERVER_COMFYUI_URL,
...cloudProxyConfig,
selfHandleResponse: true,
configure: (proxy) => {
proxy.on('proxyRes', handleGcsRedirect)
}
}
export default defineConfig({
base: DISTRIBUTION === 'cloud' ? '/' : '',
server: {
host: VITE_REMOTE_DEV ? '0.0.0.0' : undefined,
watch: {
ignored: [
'./browser_tests/**',
'./node_modules/**',
'./tests-ui/**',
'.eslintcache',
'.oxlintrc.json',
'*.config.{ts,mts}',
'**/.git/**',
'**/.github/**',
'**/.nx/**',
'**/*.{test,spec,stories}.ts',
'**/coverage/**',
'**/dist/**',
'**/playwright-report/**',
'**/test-results/**'
]
},
proxy: {
'/internal': {
target: DEV_SERVER_COMFYUI_URL,
...cloudProxyConfig
},
...(DISTRIBUTION === 'cloud'
? {
'/api/view': gcsRedirectProxyConfig,
'/api/viewvideo': gcsRedirectProxyConfig
}
: {}),
'/api': {
target: DEV_SERVER_COMFYUI_URL,
...cloudProxyConfig,
bypass: (req, res, _options) => {
if (!res) return null
// Return empty array for extensions API as these modules
// are not on vite's dev server.
if (req.url === '/api/extensions') {
res.end(JSON.stringify([]))
return false
}
// Bypass multi-user auth check from staging (cloud only)
if (DISTRIBUTION === 'cloud' && req.url === '/api/users') {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({})) // Return empty object to simulate single-user mode
return false
}
return null
}
},
'/ws': {
target: DEV_SERVER_COMFYUI_URL,
ws: true,
...cloudProxyConfig
},
'/workflow_templates': {
target: DEV_SERVER_COMFYUI_URL,
...cloudProxyConfig
},
'/extensions': {
target: DEV_SERVER_COMFYUI_URL,
changeOrigin: true,
...cloudProxyConfig
},
'/docs': {
target: DEV_SERVER_COMFYUI_URL,
changeOrigin: true,
...cloudProxyConfig
},
...(!DISABLE_TEMPLATES_PROXY
? {
'/templates': {
target: DEV_SERVER_COMFYUI_URL,
...cloudProxyConfig
}
}
: {}),
'/testsubrouteindex': {
target: 'http://localhost:5173',
rewrite: (path) => path.substring('/testsubrouteindex'.length)
}
}
},
plugins: [
...(!DISABLE_VUE_PLUGINS
? [vueDevTools(), vue(), createHtmlPlugin({})]
: [vue()]),
tailwindcss(),
typegpuPlugin({}),
comfyAPIPlugin(IS_DEV),
// Inject legacy user stylesheet links for desktop/localhost only
{
name: 'inject-user-stylesheet-links',
enforce: 'post',
transformIndexHtml(html) {
if (DISTRIBUTION === 'cloud') return html
return {
html,
tags: [
{
tag: 'link',
attrs: {
rel: 'stylesheet',
type: 'text/css',
href: 'user.css'
},
injectTo: 'head-prepend'
},
{
tag: 'link',
attrs: {
rel: 'stylesheet',
type: 'text/css',
href: 'api/userdata/user.css'
},
injectTo: 'head-prepend'
}
]
}
}
},
// Twitter/Open Graph meta tags plugin (cloud distribution only)
{
name: 'inject-twitter-meta',
transformIndexHtml(html) {
if (DISTRIBUTION !== 'cloud') return html
return {
html,
tags: [
// Basic SEO
{ tag: 'title', children: VITE_OG_TITLE, injectTo: 'head' },
{
tag: 'meta',
attrs: { name: 'description', content: VITE_OG_DESC },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { name: 'keywords', content: VITE_OG_KEYWORDS },
injectTo: 'head'
},
// Twitter Card tags
{
tag: 'meta',
attrs: { name: 'twitter:card', content: 'summary_large_image' },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { name: 'twitter:title', content: VITE_OG_TITLE },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { name: 'twitter:description', content: VITE_OG_DESC },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { name: 'twitter:image', content: VITE_OG_IMAGE },
injectTo: 'head'
},
// Open Graph tags (Twitter fallback & other platforms)
{
tag: 'meta',
attrs: { property: 'og:title', content: VITE_OG_TITLE },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { property: 'og:description', content: VITE_OG_DESC },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { property: 'og:image', content: VITE_OG_IMAGE },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { property: 'og:url', content: VITE_OG_URL },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { property: 'og:type', content: 'website' },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { property: 'og:site_name', content: 'Comfy Cloud' },
injectTo: 'head'
},
{
tag: 'meta',
attrs: { property: 'og:locale', content: 'en_US' },
injectTo: 'head'
}
]
}
}
},
Icons({
compiler: 'vue3',
customCollections: {
comfy: FileSystemIconLoader('packages/design-system/src/icons')
}
}),
Components({
dts: true,
resolvers: [
IconsResolver({
customCollections: ['comfy']
})
],
dirs: ['src/components', 'src/layout', 'src/views'],
deep: true,
extensions: ['vue'],
directoryAsNamespace: true
}),
// Bundle analyzer - generates dist/stats.html after build
// Only enabled when ANALYZE_BUNDLE=true
...(ANALYZE_BUNDLE
? [
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
template: 'treemap' // or 'sunburst', 'network'
})
]
: []),
// Sentry sourcemap upload plugin
// Only runs during cloud production builds when all Sentry env vars are present
// Requires: SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT env vars
...(DISTRIBUTION === 'cloud' &&
process.env.SENTRY_AUTH_TOKEN &&
process.env.SENTRY_ORG &&
process.env.SENTRY_PROJECT &&
!IS_DEV
? [
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
// Delete source maps after upload to prevent public access
filesToDeleteAfterUpload: ['**/*.map']
}
})
]
: [])
],
build: {
minify: SHOULD_MINIFY,
target: 'es2022',
sourcemap: GENERATE_SOURCEMAP,
rolldownOptions: {
treeshake: {
manualPureFunctions: [
'console.clear',
'console.count',
'console.countReset',
'console.debug',
'console.dir',
'console.dirxml',
'console.group',
'console.groupCollapsed',
'console.groupEnd',
'console.info',
'console.log',
'console.profile',
'console.profileEnd',
'console.table',
'console.time',
'console.timeEnd',
'console.timeLog',
'console.trace'
]
},
experimental: {
strictExecutionOrder: true
},
output: {
keepNames: true,
codeSplitting: {
groups: [
{
name: 'vendor-primevue',
test: /[\\/]node_modules[\\/](@?primevue|@primeuix)[\\/]/,
priority: 10
},
{
name: 'vendor-tiptap',
test: /[\\/]node_modules[\\/]@tiptap[\\/]/,
priority: 10
},
{
name: 'vendor-chart',
test: /[\\/]node_modules[\\/]chart\.js[\\/]/,
priority: 10
},
{
name: 'vendor-three',
test: /[\\/]node_modules[\\/](three|@sparkjsdev)[\\/]/,
priority: 10
},
{
name: 'vendor-xterm',
test: /[\\/]node_modules[\\/]@xterm[\\/]/,
priority: 10
},
{
name: 'vendor-vue',
test: /[\\/]node_modules[\\/](vue|pinia)[\\/]/,
priority: 10
},
{
name: 'vendor-reka-ui',
test: /[\\/]node_modules[\\/]reka-ui[\\/]/,
priority: 10
},
{
name: 'vendor-other',
test: /[\\/]node_modules[\\/]/,
priority: 0
}
]
}
}
}
},
define: {
__COMFYUI_FRONTEND_VERSION__: JSON.stringify(
process.env.npm_package_version
),
__SENTRY_ENABLED__: JSON.stringify(
!(process.env.NODE_ENV === 'development' || !process.env.SENTRY_DSN)
),
__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',
__DISTRIBUTION__: JSON.stringify(DISTRIBUTION),
__IS_NIGHTLY__: JSON.stringify(IS_NIGHTLY)
},
resolve: {
alias: {
'@/utils/formatUtil': '/packages/shared-frontend-utils/src/formatUtil.ts',
'@/utils/networkUtil':
'/packages/shared-frontend-utils/src/networkUtil.ts',
'@': '/src'
}
},
optimizeDeps: {
exclude: ['@comfyorg/comfyui-electron-types'],
entries: ['index.html']
},
test: {
globals: true,
environment: 'happy-dom',
setupFiles: ['./vitest.setup.ts'],
retry: process.env.CI ? 2 : 0,
include: [
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'packages/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
],
coverage: {
reporter: ['text', 'json', 'html']
},
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint}.config.*',
'**/.{oxlintrc,oxfmtrc}.json'
],
silent: 'passed-only'
}
})