Files
ComfyUI_frontend/src/services/extensionService.ts
Connor Byrne 8da221b5db refactor(ext-api): rename defineNodeExtension → defineNode, defineWidgetExtension → defineWidget
Shorter function names improve ergonomics while maintaining clarity:
- defineNode() - register node-scoped extensions
- defineWidget() - register widget type extensions

Old names kept as deprecated aliases for backwards compatibility.
Will be removed in v1.0.

Updates all docs, examples, tests, and internal references.

Addresses review discussion item #4 from design-review-12142.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-12 18:20:11 -07:00

261 lines
8.4 KiB
TypeScript

import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
import { useSettingStore } from '@/platform/settings/settingStore'
import { api } from '@/scripts/api'
import { useCommandStore } from '@/stores/commandStore'
import { useExtensionStore } from '@/stores/extensionStore'
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import type { ComfyExtension } from '@/types/comfy'
import type { AuthUserInfo } from '@/types/authTypes'
import { app } from '@/scripts/app'
import type { ComfyApp } from '@/scripts/app'
// Tracks which extensions have already received the beforeRegisterNodeDef deprecation warning
const _warnedBeforeRegisterNodeDef = new Set<string>()
export const useExtensionService = () => {
const extensionStore = useExtensionStore()
const settingStore = useSettingStore()
const keybindingStore = useKeybindingStore()
const {
wrapWithErrorHandling,
wrapWithErrorHandlingAsync,
toastErrorHandler
} = useErrorHandling()
/**
* Loads all extensions from the API into the window in parallel
*/
const loadExtensions = async () => {
extensionStore.loadDisabledExtensionNames(
settingStore.get('Comfy.Extension.Disabled')
)
const extensions = await api.getExtensions()
// Need to load core extensions first as some custom extensions
// may depend on them.
await import('../extensions/core/index')
extensionStore.captureCoreExtensions()
await Promise.all(
extensions
.filter((extension) => !extension.includes('extensions/core'))
.map(async (ext) => {
try {
await import(/* @vite-ignore */ api.fileURL(ext))
} catch (error) {
console.error('Error loading extension', ext, error)
}
})
)
}
/**
* Register an extension with the app
* @param extension The extension to register
*/
const registerExtension = (extension: ComfyExtension) => {
extensionStore.registerExtension(extension)
const addKeybinding = wrapWithErrorHandling(
keybindingStore.addDefaultKeybinding
)
const addSetting = wrapWithErrorHandling(settingStore.addSetting)
extension.keybindings?.forEach((keybinding) => {
addKeybinding(new KeybindingImpl(keybinding))
})
useCommandStore().loadExtensionCommands(extension)
useMenuItemStore().loadExtensionMenuCommands(extension)
extension.settings?.forEach(addSetting)
useBottomPanelStore().registerExtensionBottomPanelTabs(extension)
if (extension.getCustomWidgets) {
// TODO(huchenlei): We should deprecate the async return value of
// getCustomWidgets.
void (async () => {
if (extension.getCustomWidgets) {
const widgets = await extension.getCustomWidgets(app)
useWidgetStore().registerCustomWidgets(widgets)
}
})()
}
if (extension.onAuthUserResolved) {
const { onUserResolved } = useCurrentUser()
const handleUserResolved = wrapWithErrorHandlingAsync(
(user: AuthUserInfo) => extension.onAuthUserResolved?.(user, app),
(error) => {
console.error('[Extension Auth Hook Error]', {
extension: extension.name,
hook: 'onAuthUserResolved',
error
})
toastErrorHandler(error)
}
)
onUserResolved((user) => {
void handleUserResolved(user)
})
}
if (extension.onAuthTokenRefreshed) {
const { onTokenRefreshed } = useCurrentUser()
const handleTokenRefreshed = wrapWithErrorHandlingAsync(
() => extension.onAuthTokenRefreshed?.(),
(error) => {
console.error('[Extension Auth Hook Error]', {
extension: extension.name,
hook: 'onAuthTokenRefreshed',
error
})
toastErrorHandler(error)
}
)
onTokenRefreshed(() => {
void handleTokenRefreshed()
})
}
if (extension.onAuthUserLogout) {
const { onUserLogout } = useCurrentUser()
const handleUserLogout = wrapWithErrorHandlingAsync(
() => extension.onAuthUserLogout?.(),
(error) => {
console.error('[Extension Auth Hook Error]', {
extension: extension.name,
hook: 'onAuthUserLogout',
error
})
toastErrorHandler(error)
}
)
onUserLogout(() => {
void handleUserLogout()
})
}
}
type RemoveLastAppParam<T> = T extends (
...args: [...infer Rest, ComfyApp]
) => infer R
? (...args: Rest) => R
: T
type KnownExtensionMethods = Exclude<keyof ComfyExtension, number | symbol> &
string
type ComfyExtensionMethod<T extends KnownExtensionMethods> =
ComfyExtension[T] extends (...args: unknown[]) => unknown
? ComfyExtension[T]
: (...args: unknown[]) => unknown
type ComfyExtensionParamsWithoutApp<T extends KnownExtensionMethods> =
RemoveLastAppParam<ComfyExtensionMethod<T>>
/**
* Invoke an extension callback
* @param {keyof ComfyExtension} method The extension callback to execute
* @param {unknown[]} args Any arguments to pass to the callback
* @returns
*/
const invokeExtensions = <T extends KnownExtensionMethods>(
method: T,
...args: Parameters<ComfyExtensionParamsWithoutApp<T>>
) => {
const results: ReturnType<ComfyExtensionMethod<T>>[] = []
for (const ext of extensionStore.enabledExtensions) {
if (method in ext) {
try {
const fn = ext[method]
if (typeof fn === 'function') {
results.push(fn.call(ext, ...args, app))
}
} catch (error) {
console.error(
`Error calling extension '${ext.name}' method '${method}'`,
{ error },
{ extension: ext },
{ args }
)
}
}
}
return results
}
/**
* Invoke an async extension callback
* Each callback will be invoked concurrently
* @param {string} method The extension callback to execute
* @param {...unknown} args Any arguments to pass to the callback
* @returns
*/
const invokeExtensionsAsync = async <T extends KnownExtensionMethods>(
method: T,
...args: Parameters<ComfyExtensionParamsWithoutApp<T>>
) => {
return await Promise.all(
extensionStore.enabledExtensions.map(async (ext) => {
if (method in ext) {
try {
const fn = ext[method]
if (typeof fn !== 'function') {
return
}
// Set current extension name for legacy compatibility tracking
if (method === 'setup') {
legacyMenuCompat.setCurrentExtension(ext.name)
}
// DEP1: warn once per extension that uses beforeRegisterNodeDef
if (
method === 'beforeRegisterNodeDef' &&
!_warnedBeforeRegisterNodeDef.has(ext.name)
) {
_warnedBeforeRegisterNodeDef.add(ext.name)
console.warn(
`[ComfyUI] Extension "${ext.name}" uses deprecated hook "beforeRegisterNodeDef". ` +
'Use defineNode({ nodeCreated(handle) { ... } }) with a nodeTypes filter instead. ' +
'See https://docs.comfy.org/extensions/api for the v2 API.'
)
}
const result = await fn.call(ext, ...args, app)
// Clear current extension after setup
if (method === 'setup') {
legacyMenuCompat.setCurrentExtension(null)
}
return result
} catch (error) {
// Clear current extension on error too
if (method === 'setup') {
legacyMenuCompat.setCurrentExtension(null)
}
console.error(
`Error calling extension '${ext.name}' method '${method}'`,
{ error },
{ extension: ext },
{ args }
)
}
}
})
)
}
return {
loadExtensions,
registerExtension,
invokeExtensions,
invokeExtensionsAsync
}
}