mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
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>
This commit is contained in:
committed by
GitHub
parent
6afdb9529d
commit
b3da6cf1b4
@@ -1262,7 +1262,7 @@ export class LGraphCanvas
|
||||
if (!node) return
|
||||
|
||||
// TODO: This is a static method, so the below "that" appears broken.
|
||||
if (v.callback) v.callback.call(this, node, v, e, prev)
|
||||
if (v.callback) void v.callback.call(this, node, v, e, prev)
|
||||
|
||||
if (!v.value) return
|
||||
|
||||
@@ -8042,7 +8042,7 @@ export class LGraphCanvas
|
||||
}
|
||||
}
|
||||
|
||||
getCanvasMenuOptions(): IContextMenuValue<string>[] {
|
||||
getCanvasMenuOptions(): IContextMenuValue[] {
|
||||
let options: IContextMenuValue<string>[]
|
||||
if (this.getMenuOptions) {
|
||||
options = this.getMenuOptions()
|
||||
|
||||
148
src/lib/litegraph/src/contextMenuCompat.ts
Normal file
148
src/lib/litegraph/src/contextMenuCompat.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { LGraphCanvas } from './LGraphCanvas'
|
||||
import type { IContextMenuValue } from './interfaces'
|
||||
|
||||
/**
|
||||
* Simple compatibility layer for legacy getCanvasMenuOptions and getNodeMenuOptions monkey patches.
|
||||
* To disable legacy support, set ENABLE_LEGACY_SUPPORT = false
|
||||
*/
|
||||
const ENABLE_LEGACY_SUPPORT = true
|
||||
|
||||
type ContextMenuValueProvider = (...args: unknown[]) => IContextMenuValue[]
|
||||
|
||||
class LegacyMenuCompat {
|
||||
private originalMethods = new Map<string, ContextMenuValueProvider>()
|
||||
private hasWarned = new Set<string>()
|
||||
private currentExtension: string | null = null
|
||||
private isExtracting = false
|
||||
private readonly wrapperMethods = new Map<string, ContextMenuValueProvider>()
|
||||
private readonly preWrapperMethods = new Map<
|
||||
string,
|
||||
ContextMenuValueProvider
|
||||
>()
|
||||
private readonly wrapperInstalled = new Map<string, boolean>()
|
||||
|
||||
/**
|
||||
* Set the name of the extension that is currently being set up.
|
||||
* This allows us to track which extension is monkey-patching.
|
||||
* @param extensionName The name of the extension
|
||||
*/
|
||||
setCurrentExtension(extensionName: string | null) {
|
||||
this.currentExtension = extensionName
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a wrapper method that should NOT be treated as a legacy monkey-patch.
|
||||
* @param methodName The method name
|
||||
* @param wrapperFn The wrapper function
|
||||
* @param preWrapperFn The method that existed before the wrapper
|
||||
* @param prototype The prototype to verify wrapper installation
|
||||
*/
|
||||
registerWrapper(
|
||||
methodName: keyof LGraphCanvas,
|
||||
wrapperFn: ContextMenuValueProvider,
|
||||
preWrapperFn: ContextMenuValueProvider,
|
||||
prototype?: LGraphCanvas
|
||||
) {
|
||||
this.wrapperMethods.set(methodName, wrapperFn)
|
||||
this.preWrapperMethods.set(methodName, preWrapperFn)
|
||||
const isInstalled = prototype && prototype[methodName] === wrapperFn
|
||||
this.wrapperInstalled.set(methodName, !!isInstalled)
|
||||
}
|
||||
|
||||
/**
|
||||
* Install compatibility layer to detect monkey-patching
|
||||
* @param prototype The prototype to install on
|
||||
* @param methodName The method name to track
|
||||
*/
|
||||
install(prototype: LGraphCanvas, methodName: keyof LGraphCanvas) {
|
||||
if (!ENABLE_LEGACY_SUPPORT) return
|
||||
|
||||
const originalMethod = prototype[methodName]
|
||||
this.originalMethods.set(methodName, originalMethod)
|
||||
|
||||
let currentImpl = originalMethod
|
||||
|
||||
Object.defineProperty(prototype, methodName, {
|
||||
get() {
|
||||
return currentImpl
|
||||
},
|
||||
set: (newImpl: ContextMenuValueProvider) => {
|
||||
const fnKey = `${methodName}:${newImpl.toString().slice(0, 100)}`
|
||||
if (!this.hasWarned.has(fnKey) && this.currentExtension) {
|
||||
this.hasWarned.add(fnKey)
|
||||
|
||||
console.warn(
|
||||
`%c[DEPRECATED]%c Monkey-patching ${methodName} is deprecated. (Extension: "${this.currentExtension}")\n` +
|
||||
`Please use the new context menu API instead.\n\n` +
|
||||
`See: https://docs.comfy.org/custom-nodes/js/context-menu-migration`,
|
||||
'color: orange; font-weight: bold',
|
||||
'color: inherit'
|
||||
)
|
||||
}
|
||||
currentImpl = newImpl
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract items that were added by legacy monkey patches
|
||||
* @param methodName The method name that was monkey-patched
|
||||
* @param context The context to call methods with
|
||||
* @param args Arguments to pass to the methods
|
||||
* @returns Array of menu items added by monkey patches
|
||||
*/
|
||||
extractLegacyItems(
|
||||
methodName: keyof LGraphCanvas,
|
||||
context: LGraphCanvas,
|
||||
...args: unknown[]
|
||||
): IContextMenuValue[] {
|
||||
if (!ENABLE_LEGACY_SUPPORT) return []
|
||||
if (this.isExtracting) return []
|
||||
|
||||
const originalMethod = this.originalMethods.get(methodName)
|
||||
if (!originalMethod) return []
|
||||
|
||||
try {
|
||||
this.isExtracting = true
|
||||
|
||||
const originalItems = originalMethod.apply(context, args) as
|
||||
| IContextMenuValue[]
|
||||
| undefined
|
||||
if (!originalItems) return []
|
||||
|
||||
const currentMethod = context.constructor.prototype[methodName]
|
||||
if (!currentMethod || currentMethod === originalMethod) return []
|
||||
|
||||
const registeredWrapper = this.wrapperMethods.get(methodName)
|
||||
if (registeredWrapper && currentMethod === registeredWrapper) return []
|
||||
|
||||
const preWrapperMethod = this.preWrapperMethods.get(methodName)
|
||||
const wrapperWasInstalled = this.wrapperInstalled.get(methodName)
|
||||
|
||||
const shouldSkipWrapper =
|
||||
preWrapperMethod &&
|
||||
wrapperWasInstalled &&
|
||||
currentMethod !== preWrapperMethod
|
||||
|
||||
const methodToCall = shouldSkipWrapper ? preWrapperMethod : currentMethod
|
||||
|
||||
const patchedItems = methodToCall.apply(context, args) as
|
||||
| IContextMenuValue[]
|
||||
| undefined
|
||||
if (!patchedItems) return []
|
||||
|
||||
if (patchedItems.length > originalItems.length) {
|
||||
return patchedItems.slice(originalItems.length) as IContextMenuValue[]
|
||||
}
|
||||
|
||||
return []
|
||||
} catch (e) {
|
||||
console.error('[Context Menu Compat] Failed to extract legacy items:', e)
|
||||
return []
|
||||
} finally {
|
||||
this.isExtracting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const legacyMenuCompat = new LegacyMenuCompat()
|
||||
@@ -393,7 +393,7 @@ export interface IContextMenuOptions<TValue = unknown, TExtra = unknown>
|
||||
event?: MouseEvent,
|
||||
previous_menu?: ContextMenu<TValue>,
|
||||
extra?: unknown
|
||||
): void | boolean
|
||||
): void | boolean | Promise<void | boolean>
|
||||
}
|
||||
|
||||
export interface IContextMenuValue<
|
||||
@@ -416,7 +416,7 @@ export interface IContextMenuValue<
|
||||
event?: MouseEvent,
|
||||
previous_menu?: ContextMenu<TValue>,
|
||||
extra?: TExtra
|
||||
): void | boolean
|
||||
): void | boolean | Promise<void | boolean>
|
||||
}
|
||||
|
||||
interface IContextMenuSubmenu<TValue = unknown>
|
||||
|
||||
Reference in New Issue
Block a user