feat(contextMenu): add legacy compatibility layer for monkey-patched extensions

Adds a compatibility layer that detects and supports legacy extensions using the monkey-patching pattern, while warning developers about the deprecated approach.

**Features:**
- Automatic detection of monkey-patched context menu methods
- Console warnings with extension name for deprecated patterns
- Extraction and integration of legacy menu items
- Extension tracking during setup for accurate warnings

**Files:**
- `src/lib/litegraph/src/contextMenuCompat.ts`: Core compatibility logic
- `src/services/extensionService.ts`: Extension name tracking
- `src/composables/useContextMenuTranslation.ts`: Integration layer
- Comprehensive test coverage

Depends on PR #5977 (context menu extension API)
This commit is contained in:
Johnpaul
2025-10-09 01:36:29 +01:00
parent 5b37fc59e7
commit 0685a1da3c
4 changed files with 378 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
import { st, te } from '@/i18n'
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
import type {
IContextMenuOptions,
IContextMenuValue,
@@ -6,18 +7,42 @@ import type {
IWidget
} from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { normalizeI18nKey } from '@/utils/formatUtil'
/**
* Add translation for litegraph context menu.
*/
export const useContextMenuTranslation = () => {
// Install compatibility layer BEFORE any extensions load
legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions')
const f = LGraphCanvas.prototype.getCanvasMenuOptions
const getCanvasCenterMenuOptions = function (
this: LGraphCanvas,
...args: Parameters<typeof f>
) {
const res = f.apply(this, args) as ReturnType<typeof f>
// Add items from new extension API
const newApiItems = app.collectCanvasMenuItems(this)
for (const item of newApiItems) {
// @ts-expect-error - Generic types differ but runtime compatibility is ensured
res.push(item)
}
// Add legacy monkey-patched items
const legacyItems = legacyMenuCompat.extractLegacyItems(
'getCanvasMenuOptions',
this,
...args
)
for (const item of legacyItems) {
// @ts-expect-error - Generic types differ but runtime compatibility is ensured
res.push(item)
}
// Translate all items
for (const item of res) {
if (item?.content) {
item.content = st(`contextMenu.${item.content}`, item.content)

View File

@@ -0,0 +1,115 @@
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 AnyFunction = (...args: any[]) => any
class LegacyMenuCompat {
private originalMethods = new Map<string, AnyFunction>()
private hasWarned = new Set<string>()
private currentExtension: string | null = null
/**
* 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
}
/**
* Install compatibility layer to detect monkey-patching
* @param prototype The prototype to install on (e.g., LGraphCanvas.prototype)
* @param methodName The method name to track (e.g., 'getCanvasMenuOptions')
*/
install(prototype: any, methodName: string) {
if (!ENABLE_LEGACY_SUPPORT) return
// Store original
const originalMethod = prototype[methodName]
this.originalMethods.set(methodName, originalMethod)
// Wrap with getter/setter to detect patches
let currentImpl = originalMethod
Object.defineProperty(prototype, methodName, {
get() {
return currentImpl
},
set: (newImpl: AnyFunction) => {
// Log once per unique function
const fnKey = `${methodName}:${newImpl.toString().slice(0, 100)}`
if (!this.hasWarned.has(fnKey)) {
this.hasWarned.add(fnKey)
const extensionInfo = this.currentExtension
? ` (Extension: "${this.currentExtension}")`
: ''
console.warn(
`%c[DEPRECATED]%c Monkey-patching ${methodName} is deprecated.${extensionInfo}\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
},
configurable: true
})
}
/**
* 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 (e.g., canvas instance)
* @param args Arguments to pass to the methods
* @returns Array of menu items added by monkey patches
*/
extractLegacyItems(
methodName: string,
context: any,
...args: any[]
): IContextMenuValue[] {
if (!ENABLE_LEGACY_SUPPORT) return []
const originalMethod = this.originalMethods.get(methodName)
if (!originalMethod) return []
try {
// Get baseline from original
const originalItems = originalMethod.apply(context, args) as
| IContextMenuValue[]
| undefined
if (!originalItems) return []
// Get current method (potentially patched)
const currentMethod = context.constructor.prototype[methodName]
if (!currentMethod || currentMethod === originalMethod) return []
// Get items from patched method
const patchedItems = currentMethod.apply(context, args) as
| IContextMenuValue[]
| undefined
if (!patchedItems) return []
// Return items that were added (simple slice approach)
if (patchedItems.length > originalItems.length) {
return patchedItems.slice(originalItems.length)
}
return []
} catch (e) {
console.error('[Context Menu Compat] Failed to extract legacy items:', e)
return []
}
}
}
export const legacyMenuCompat = new LegacyMenuCompat()

View File

@@ -1,5 +1,6 @@
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'
@@ -122,8 +123,25 @@ export const useExtensionService = () => {
extensionStore.enabledExtensions.map(async (ext) => {
if (method in ext) {
try {
return await ext[method](...args, app)
// 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 },