Files
ComfyUI_frontend/src/services/extensionService.ts
Johnpaul Chiwetelu b3da6cf1b4 Contextmenu extension migration (#5993)
This pull request refactors how context menu items are contributed by
extensions in the LiteGraph-based canvas. The legacy monkey-patching
approach for adding context menu options is replaced by a new, explicit
API (`getCanvasMenuItems` and `getNodeMenuItems`) for extensions. A
compatibility layer is added to support legacy extensions and warn
developers about deprecated usage. The changes improve maintainability,
extension interoperability, and migration to the new context menu
system.

### Context Menu System Refactor

* Introduced a new API for extensions to contribute context menu items
via `getCanvasMenuItems` and `getNodeMenuItems` methods, replacing
legacy monkey-patching of `LGraphCanvas.prototype.getCanvasMenuOptions`.
Major extension files (`groupNode.ts`, `groupOptions.ts`,
`nodeTemplates.ts`) now use this new API.
[[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917L1779-R1771)
[[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL232-R239)
[[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL447-R458)
* Added a compatibility layer (`legacyMenuCompat` in
`contextMenuCompat.ts`) to detect and warn when legacy monkey-patching
is used, and to extract legacy-added menu items for backward
compatibility.
[[1]](diffhunk://#diff-2b724cb107c04e290369fb927e2ae9fad03be9e617a7d4de2487deab89d0d018R2-R45)
[[2]](diffhunk://#diff-d3a8284ec16ae3f9512e33abe44ae653ed1aa45c9926485ef6270cc8d2b94ae6R1-R115)

### Extension Migration

* Refactored core extensions (`groupNode`, `groupOptions`, and
`nodeTemplates`) to implement the new context menu API, moving menu item
logic out of monkey-patched methods and into explicit extension methods.
[[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917L1633-L1683)
[[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL19-R77)
[[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL366-R373)

### Type and Import Cleanup

* Updated imports for context menu types (`IContextMenuValue`) across
affected files for consistency with the new API.
[[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917R4-L7)
[[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL1-R11)
[[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL2-R6)
[[4]](diffhunk://#diff-bde0dce9fe2403685d27b0e94a938c3d72824d02d01d1fd6167a0dddc6e585ddR10)

### Backward Compatibility and Migration Guidance

* The compatibility layer logs a deprecation warning to the console when
legacy monkey-patching is detected, helping developers migrate to the
new API.

---

These changes collectively modernize the context menu extension
mechanism, improve code clarity, and provide a migration path for legacy
extensions.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5993-Contextmenu-extension-migration-2876d73d3650813fae07c1141679637a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-28 04:02:28 +01:00

178 lines
5.5 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 { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useExtensionStore } from '@/stores/extensionStore'
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import type { ComfyExtension } from '@/types/comfy'
export const useExtensionService = () => {
const extensionStore = useExtensionStore()
const settingStore = useSettingStore()
const keybindingStore = useKeybindingStore()
const { wrapWithErrorHandling } = 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()
onUserResolved((user) => {
void extension.onAuthUserResolved?.(user, app)
})
}
if (extension.onAuthTokenRefreshed) {
const { onTokenRefreshed } = useCurrentUser()
onTokenRefreshed(() => {
void extension.onAuthTokenRefreshed?.()
})
}
if (extension.onAuthUserLogout) {
const { onUserLogout } = useCurrentUser()
onUserLogout(() => {
void extension.onAuthUserLogout?.()
})
}
}
/**
* Invoke an extension callback
* @param {keyof ComfyExtension} method The extension callback to execute
* @param {any[]} args Any arguments to pass to the callback
* @returns
*/
const invokeExtensions = (method: keyof ComfyExtension, ...args: any[]) => {
const results: any[] = []
for (const ext of extensionStore.enabledExtensions) {
if (method in ext) {
try {
results.push(ext[method](...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 {...any} args Any arguments to pass to the callback
* @returns
*/
const invokeExtensionsAsync = async (
method: keyof ComfyExtension,
...args: any[]
) => {
return await Promise.all(
extensionStore.enabledExtensions.map(async (ext) => {
if (method in ext) {
try {
// Set current extension name for legacy compatibility tracking
if (method === 'setup') {
legacyMenuCompat.setCurrentExtension(ext.name)
}
const result = await ext[method](...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
}
}