feat: No Explicit Any (#8601)

## Summary
- Add `typescript/no-explicit-any` rule to `.oxlintrc.json` to enforce
no explicit `any` types
- Fix all 40 instances of explicit `any` throughout the codebase
- Improve type safety with proper TypeScript types

## Changes Made

### Configuration
- Added `typescript/no-explicit-any` rule to `.oxlintrc.json`

### Type Fixes
- Replaced `any` with `unknown` for truly unknown types
- Updated generic type parameters to use `unknown` defaults instead of
`any`
- Fixed method `this` parameters to avoid variance issues
- Updated component props to match new generic types
- Fixed test mocks to use proper type assertions

### Key Files Modified
- `src/types/treeExplorerTypes.ts`: Updated TreeExplorerNode interface
generics
- `src/platform/settings/types.ts`: Fixed SettingParams generic default
- `src/lib/litegraph/src/LGraph.ts`: Fixed ParamsArray type constraint
- `src/extensions/core/electronAdapter.ts`: Fixed onChange callbacks
- `src/views/GraphView.vue`: Added proper type imports
- Multiple test files: Fixed type assertions and mocks

## Test Plan
- [x] All lint checks pass (`pnpm lint`)
- [x] TypeScript compilation succeeds (`pnpm typecheck`)
- [x] Pre-commit hooks pass
- [x] No regression in functionality

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8601-feat-add-typescript-no-explicit-any-rule-and-fix-all-instances-2fd6d73d365081fd9beef75d5a6daf5b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
Johnpaul Chiwetelu
2026-02-12 00:13:48 +01:00
committed by GitHub
parent 92b7437d86
commit 4fc1d2ef5b
28 changed files with 242 additions and 151 deletions

View File

@@ -96,6 +96,7 @@
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error",
"typescript/no-explicit-any": "error",
"vue/no-import-compiler-macros": "error",
"vue/no-dupe-keys": "error"
},

View File

@@ -12,9 +12,9 @@
nodeContent: ({ context }) => ({
class: 'group/tree-node',
onClick: (e: MouseEvent) =>
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
onNodeContentClick(e, context.node as RenderedTreeExplorerNode<T>),
onContextmenu: (e: MouseEvent) =>
handleContextMenu(e, context.node as RenderedTreeExplorerNode)
handleContextMenu(e, context.node as RenderedTreeExplorerNode<T>)
}),
nodeToggleButton: () => ({
onClick: (e: MouseEvent) => {
@@ -36,15 +36,11 @@
</Tree>
<ContextMenu ref="menu" :model="menuItems" />
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
<script setup lang="ts" generic="T">
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
import Tree from 'primevue/tree'
import { computed, provide, ref } from 'vue'
import { computed, provide, ref, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
@@ -60,6 +56,10 @@ import type {
} from '@/types/treeExplorerTypes'
import { combineTrees, findNodeByKey } from '@/utils/treeUtil'
defineOptions({
inheritAttrs: false
})
const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys', {
required: true
})
@@ -69,13 +69,13 @@ const selectionKeys = defineModel<Record<string, boolean>>('selectionKeys')
const storeSelectionKeys = selectionKeys.value !== undefined
const props = defineProps<{
root: TreeExplorerNode
root: TreeExplorerNode<T>
class?: string
}>()
const emit = defineEmits<{
(e: 'nodeClick', node: RenderedTreeExplorerNode, event: MouseEvent): void
(e: 'nodeDelete', node: RenderedTreeExplorerNode): void
(e: 'contextMenu', node: RenderedTreeExplorerNode, event: MouseEvent): void
(e: 'nodeClick', node: RenderedTreeExplorerNode<T>, event: MouseEvent): void
(e: 'nodeDelete', node: RenderedTreeExplorerNode<T>): void
(e: 'contextMenu', node: RenderedTreeExplorerNode<T>, event: MouseEvent): void
}>()
const {
@@ -83,19 +83,19 @@ const {
getAddFolderMenuItem,
handleFolderCreation,
addFolderCommand
} = useTreeFolderOperations(
/* expandNode */ (node: TreeExplorerNode) => {
} = useTreeFolderOperations<T>(
/* expandNode */ (node: TreeExplorerNode<T>) => {
expandedKeys.value[node.key] = true
}
)
const renderedRoot = computed<RenderedTreeExplorerNode>(() => {
const renderedRoot = computed<RenderedTreeExplorerNode<T>>(() => {
const renderedRoot = fillNodeInfo(props.root)
return newFolderNode.value
? combineTrees(renderedRoot, newFolderNode.value)
: renderedRoot
})
const getTreeNodeIcon = (node: TreeExplorerNode) => {
const getTreeNodeIcon = (node: TreeExplorerNode<T>) => {
if (node.getIcon) {
const icon = node.getIcon()
if (icon) {
@@ -111,7 +111,9 @@ const getTreeNodeIcon = (node: TreeExplorerNode) => {
const isExpanded = expandedKeys.value?.[node.key] ?? false
return isExpanded ? 'pi pi-folder-open' : 'pi pi-folder'
}
const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
const fillNodeInfo = (
node: TreeExplorerNode<T>
): RenderedTreeExplorerNode<T> => {
const children = node.children?.map(fillNodeInfo) ?? []
const totalLeaves = node.leaf
? 1
@@ -128,7 +130,7 @@ const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
}
const onNodeContentClick = async (
e: MouseEvent,
node: RenderedTreeExplorerNode
node: RenderedTreeExplorerNode<T>
) => {
if (!storeSelectionKeys) {
selectionKeys.value = {}
@@ -139,20 +141,22 @@ const onNodeContentClick = async (
emit('nodeClick', node, e)
}
const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null)
const menuTargetNode = shallowRef<RenderedTreeExplorerNode<T> | null>(null)
const extraMenuItems = computed(() => {
return menuTargetNode.value?.contextMenuItems
? typeof menuTargetNode.value.contextMenuItems === 'function'
? menuTargetNode.value.contextMenuItems(menuTargetNode.value)
: menuTargetNode.value.contextMenuItems
const node = menuTargetNode.value
return node?.contextMenuItems
? typeof node.contextMenuItems === 'function'
? node.contextMenuItems(node)
: node.contextMenuItems
: []
})
const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null)
const renameEditingNode = shallowRef<RenderedTreeExplorerNode<T> | null>(null)
const errorHandling = useErrorHandling()
const handleNodeLabelEdit = async (
node: RenderedTreeExplorerNode,
n: RenderedTreeExplorerNode,
newName: string
) => {
const node = n as RenderedTreeExplorerNode<T>
await errorHandling.wrapWithErrorHandlingAsync(
async () => {
if (node.key === newFolderNode.value?.key) {
@@ -170,35 +174,36 @@ const handleNodeLabelEdit = async (
provide(InjectKeyHandleEditLabelFunction, handleNodeLabelEdit)
const { t } = useI18n()
const renameCommand = (node: RenderedTreeExplorerNode) => {
const renameCommand = (node: RenderedTreeExplorerNode<T>) => {
renameEditingNode.value = node
}
const deleteCommand = async (node: RenderedTreeExplorerNode) => {
const deleteCommand = async (node: RenderedTreeExplorerNode<T>) => {
await node.handleDelete?.()
emit('nodeDelete', node)
}
const menuItems = computed<MenuItem[]>(() =>
[
getAddFolderMenuItem(menuTargetNode.value),
const menuItems = computed<MenuItem[]>(() => {
const node = menuTargetNode.value
return [
getAddFolderMenuItem(node),
{
label: t('g.rename'),
icon: 'pi pi-file-edit',
command: () => {
if (menuTargetNode.value) {
renameCommand(menuTargetNode.value)
if (node) {
renameCommand(node)
}
},
visible: menuTargetNode.value?.handleRename !== undefined
visible: node?.handleRename !== undefined
},
{
label: t('g.delete'),
icon: 'pi pi-trash',
command: async () => {
if (menuTargetNode.value) {
await deleteCommand(menuTargetNode.value)
if (node) {
await deleteCommand(node)
}
},
visible: menuTargetNode.value?.handleDelete !== undefined,
visible: node?.handleDelete !== undefined,
isAsync: true // The delete command can be async
},
...extraMenuItems.value
@@ -210,9 +215,12 @@ const menuItems = computed<MenuItem[]>(() =>
})
: undefined
}))
)
})
const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
const handleContextMenu = (
e: MouseEvent,
node: RenderedTreeExplorerNode<T>
) => {
menuTargetNode.value = node
emit('contextMenu', node, e)
if (menuItems.value.filter((item) => item.visible).length > 0) {
@@ -224,15 +232,13 @@ const wrapCommandWithErrorHandler = (
command: (event: MenuItemCommandEvent) => void,
{ isAsync = false }: { isAsync: boolean }
) => {
const node = menuTargetNode.value
return isAsync
? errorHandling.wrapWithErrorHandlingAsync(
command as (event: MenuItemCommandEvent) => Promise<void>,
menuTargetNode.value?.handleError
)
: errorHandling.wrapWithErrorHandling(
command,
menuTargetNode.value?.handleError
node?.handleError
)
: errorHandling.wrapWithErrorHandling(command, node?.handleError)
}
defineExpose({

View File

@@ -36,7 +36,7 @@
</div>
</template>
<script setup lang="ts">
<script setup lang="ts" generic="T">
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
import Badge from 'primevue/badge'
import { computed, inject, ref } from 'vue'
@@ -53,17 +53,17 @@ import type {
} from '@/types/treeExplorerTypes'
const props = defineProps<{
node: RenderedTreeExplorerNode
node: RenderedTreeExplorerNode<T>
}>()
const emit = defineEmits<{
(
e: 'itemDropped',
node: RenderedTreeExplorerNode,
data: RenderedTreeExplorerNode
node: RenderedTreeExplorerNode<T>,
data: RenderedTreeExplorerNode<T>
): void
(e: 'dragStart', node: RenderedTreeExplorerNode): void
(e: 'dragEnd', node: RenderedTreeExplorerNode): void
(e: 'dragStart', node: RenderedTreeExplorerNode<T>): void
(e: 'dragEnd', node: RenderedTreeExplorerNode<T>): void
}>()
const nodeBadgeText = computed<string>(() => {
@@ -80,7 +80,7 @@ const showNodeBadgeText = computed<boolean>(() => nodeBadgeText.value !== '')
const isEditing = computed<boolean>(() => props.node.isEditingLabel ?? false)
const handleEditLabel = inject(InjectKeyHandleEditLabelFunction)
const handleRename = (newName: string) => {
handleEditLabel?.(props.node, newName)
handleEditLabel?.(props.node as RenderedTreeExplorerNode, newName)
}
const container = ref<HTMLElement | null>(null)
@@ -117,9 +117,13 @@ if (props.node.droppable) {
onDrop: async (event) => {
const dndData = event.source.data as TreeExplorerDragAndDropData
if (dndData.type === 'tree-explorer-node') {
await props.node.handleDrop?.(dndData)
await props.node.handleDrop?.(dndData as TreeExplorerDragAndDropData<T>)
canDrop.value = false
emit('itemDropped', props.node, dndData.data)
emit(
'itemDropped',
props.node,
dndData.data as RenderedTreeExplorerNode<T>
)
}
},
onDragEnter: (event) => {

View File

@@ -50,7 +50,8 @@
<template #before-label="{ node: treeNode }">
<span
v-if="
treeNode.data?.isModified || !treeNode.data?.isPersisted
(treeNode.data as ComfyWorkflow)?.isModified ||
!(treeNode.data as ComfyWorkflow)?.isPersisted
"
>*</span
>

View File

@@ -215,7 +215,11 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
}
)
const treeExplorerRef = ref<InstanceType<typeof TreeExplorer> | null>(null)
interface TreeExplorerExposed {
addFolderCommand: (targetNodeKey: string) => void
}
const treeExplorerRef = ref<TreeExplorerExposed | null>(null)
defineExpose({
addNewBookmarkFolder: () => treeExplorerRef.value?.addFolderCommand('root')
})

View File

@@ -63,7 +63,7 @@ onUnmounted(() => {
})
const expandedKeys = inject(InjectKeyExpandedKeys)
const handleItemDrop = (node: RenderedTreeExplorerNode) => {
const handleItemDrop = (node: RenderedTreeExplorerNode<ComfyNodeDefImpl>) => {
if (!expandedKeys) return
expandedKeys.value[node.key] = true
}

View File

@@ -1,5 +1,5 @@
import type { MenuItem } from 'primevue/menuitem'
import { ref } from 'vue'
import { shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
@@ -8,12 +8,14 @@ import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
* Use this to handle folder operations in a tree.
* @param expandNode - The function to expand a node.
*/
export function useTreeFolderOperations(
expandNode: (node: RenderedTreeExplorerNode) => void
export function useTreeFolderOperations<T>(
expandNode: (node: RenderedTreeExplorerNode<T>) => void
) {
const { t } = useI18n()
const newFolderNode = ref<RenderedTreeExplorerNode | null>(null)
const addFolderTargetNode = ref<RenderedTreeExplorerNode | null>(null)
const newFolderNode = shallowRef<RenderedTreeExplorerNode<T> | null>(null)
const addFolderTargetNode = shallowRef<RenderedTreeExplorerNode<T> | null>(
null
)
// Generate a unique temporary key for the new folder
const generateTempKey = (parentKey: string) => {
@@ -37,7 +39,7 @@ export function useTreeFolderOperations(
* The command to add a folder to a node via the context menu
* @param targetNode - The node where the folder will be added under
*/
const addFolderCommand = (targetNode: RenderedTreeExplorerNode) => {
const addFolderCommand = (targetNode: RenderedTreeExplorerNode<T>) => {
expandNode(targetNode)
newFolderNode.value = {
key: generateTempKey(targetNode.key),
@@ -49,13 +51,13 @@ export function useTreeFolderOperations(
totalLeaves: 0,
badgeText: '',
isEditingLabel: true
}
} as RenderedTreeExplorerNode<T>
addFolderTargetNode.value = targetNode
}
// Generate the "Add Folder" menu item
const getAddFolderMenuItem = (
targetNode: RenderedTreeExplorerNode | null
targetNode: RenderedTreeExplorerNode<T> | null
): MenuItem => {
return {
label: t('g.newFolder'),

View File

@@ -9,7 +9,7 @@ import {
VramManagement
} from '@/types/serverArgs'
export type ServerConfigValue = string | number | true | null | undefined
export type ServerConfigValue = string | number | boolean | null | undefined
export interface ServerConfig<T> extends FormItem {
id: string
@@ -19,7 +19,7 @@ export interface ServerConfig<T> extends FormItem {
getValue?: (value: T) => Record<string, ServerConfigValue>
}
export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
export const SERVER_CONFIG_ITEMS = [
// Network settings
{
id: 'listen',

View File

@@ -19,7 +19,7 @@ import { electronAPI as getElectronAPI } from '@/utils/envUtil'
const toastStore = useToastStore()
const { staticUrls, buildDocsUrl } = useExternalLink()
const onChangeRestartApp = (newValue: string, oldValue: string) => {
const onChangeRestartApp = (newValue: unknown, oldValue: unknown) => {
// Add a delay to allow changes to take effect before restarting.
if (oldValue !== undefined && newValue !== oldValue) {
electronAPI.restartApp('Restart ComfyUI to apply changes.', 1500)

View File

@@ -1,3 +1,4 @@
import type { ComfyExtension } from '@/types/comfy'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { app } from '../../scripts/app'
@@ -5,10 +6,17 @@ import { ComfyWidgets } from '../../scripts/widgets'
// Adds defaults for quickly adding nodes with middle click on the input/output
interface SlotDefaultsExtension extends ComfyExtension {
suggestionsNumber: { value: number } | null
slot_types_default_out: Record<string, string[]>
slot_types_default_in: Record<string, string[]>
setDefaults(maxNum?: number | null): void
}
app.registerExtension({
name: 'Comfy.SlotDefaults',
suggestionsNumber: null,
init() {
init(this: SlotDefaultsExtension) {
LiteGraph.search_filter_enabled = true
LiteGraph.middle_click_slot_add_default_node = true
this.suggestionsNumber = app.ui.settings.addSetting({
@@ -24,13 +32,13 @@ app.registerExtension({
},
defaultValue: 5,
onChange: (newVal) => {
this.setDefaults(newVal)
this.setDefaults(newVal as number)
}
})
},
slot_types_default_out: {},
slot_types_default_in: {},
async beforeRegisterNodeDef(nodeType, nodeData) {
async beforeRegisterNodeDef(this: SlotDefaultsExtension, nodeType, nodeData) {
var nodeId = nodeData.name
const inputs = nodeData['input']?.['required'] //only show required inputs to reduce the mess also not logical to create node with optional inputs
for (const inputKey in inputs) {
@@ -83,22 +91,23 @@ app.registerExtension({
}
}
var maxNum = this.suggestionsNumber.value
var maxNum = this.suggestionsNumber?.value
this.setDefaults(maxNum)
},
setDefaults(maxNum?: number | null) {
setDefaults(this: SlotDefaultsExtension, maxNum?: number | null) {
LiteGraph.slot_types_default_out = {}
LiteGraph.slot_types_default_in = {}
const max = maxNum ?? undefined
for (const type in this.slot_types_default_out) {
LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[
type
].slice(0, maxNum)
].slice(0, max)
}
for (const type in this.slot_types_default_in) {
LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[
type
].slice(0, maxNum)
].slice(0, max)
}
}
})

View File

@@ -90,12 +90,13 @@ export interface LGraphState {
lastRerouteId: number
}
type ParamsArray<
T extends Record<any, any>,
K extends MethodNames<T>
> = Parameters<T[K]>[1] extends undefined
? Parameters<T[K]> | Parameters<T[K]>[0]
: Parameters<T[K]>
type ParamsArray<T, K extends MethodNames<T>> = Parameters<
Extract<T[K], (...args: never[]) => unknown>
>[1] extends undefined
?
| Parameters<Extract<T[K], (...args: never[]) => unknown>>
| Parameters<Extract<T[K], (...args: never[]) => unknown>>[0]
: Parameters<Extract<T[K], (...args: never[]) => unknown>>
/** Configuration used by {@link LGraph} `config`. */
export interface LGraphConfig {

View File

@@ -72,7 +72,7 @@ import FormItem from '@/components/common/FormItem.vue'
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import type { ServerConfig } from '@/constants/serverConfig'
import type { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { FormItem as FormItemType } from '@/platform/settings/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -133,7 +133,7 @@ onBeforeUnmount(() => {
})
})
const translateItem = (item: ServerConfig<any>): FormItemType => {
const translateItem = (item: ServerConfig<ServerConfigValue>): FormItemType => {
return {
...item,
name: t(`serverConfigItems.${item.id}.name`, item.name),

View File

@@ -165,7 +165,9 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultsByInstallVersion: {
'1.25.0': 'legacy'
},
onChange: async (newValue: string, oldValue?: string) => {
onChange: async (val: unknown, old?: unknown) => {
const newValue = val as string
const oldValue = old as string | undefined
if (!oldValue) return
const settingStore = useSettingStore()
@@ -194,7 +196,8 @@ export const CORE_SETTINGS: SettingParams[] = [
{ value: 'select', text: 'Select' }
],
versionAdded: '1.27.4',
onChange: async (newValue: string) => {
onChange: async (val: unknown) => {
const newValue = val as string
const settingStore = useSettingStore()
const navigationMode = settingStore.get('Comfy.Canvas.NavigationMode')
@@ -223,7 +226,8 @@ export const CORE_SETTINGS: SettingParams[] = [
{ value: 'zoom', text: 'Zoom in/out' }
],
versionAdded: '1.27.4',
onChange: async (newValue: string) => {
onChange: async (val: unknown) => {
const newValue = val as string
const settingStore = useSettingStore()
const navigationMode = settingStore.get('Comfy.Canvas.NavigationMode')
@@ -569,7 +573,8 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'combo',
options: ['Disabled', 'Top'],
tooltip: 'Enable the redesigned top menu bar.',
migrateDeprecatedValue: (value: string) => {
migrateDeprecatedValue: (val: unknown) => {
const value = val as string
// Floating is now supported by dragging the docked actionbar off.
if (value === 'Floating') {
return 'Top'
@@ -585,7 +590,8 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'combo',
options: ['Sidebar', 'Topbar'],
defaultValue: 'Topbar',
migrateDeprecatedValue: (value: string) => {
migrateDeprecatedValue: (val: unknown) => {
const value = val as string
if (value === 'Topbar (2nd-row)') {
return 'Topbar'
}
@@ -615,9 +621,8 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultValue: [] as Keybinding[],
versionAdded: '1.3.7',
versionModified: '1.7.3',
migrateDeprecatedValue: (
value: (Keybinding & { targetSelector?: string })[]
) => {
migrateDeprecatedValue: (val: unknown) => {
const value = val as (Keybinding & { targetSelector?: string })[]
return value.map((keybinding) => {
if (keybinding.targetSelector === '#graph-canvas') {
keybinding.targetElementId = 'graph-canvas-container'
@@ -886,7 +891,8 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'hidden',
defaultValue: 'dark',
versionModified: '1.6.7',
migrateDeprecatedValue(value: string) {
migrateDeprecatedValue(val: unknown) {
const value = val as string
// Legacy custom palettes were prefixed with 'custom_'
return value.startsWith('custom_') ? value.replace('custom_', '') : value
}

View File

@@ -118,7 +118,7 @@ describe('useSettingStore', () => {
name: 'test.setting',
type: 'text',
defaultValue: 'default',
migrateDeprecatedValue: (value: string) => value.toUpperCase()
migrateDeprecatedValue: (val: unknown) => (val as string).toUpperCase()
}
store.settingValues['test.setting'] = 'oldvalue'

View File

@@ -26,11 +26,11 @@ export interface SettingOption {
value?: string | number
}
export interface SettingParams<TValue = any> extends FormItem {
export interface SettingParams<TValue = unknown> extends FormItem {
id: keyof Settings
defaultValue: TValue | (() => TValue)
defaultsByInstallVersion?: Record<`${number}.${number}.${number}`, TValue>
onChange?: (newValue: TValue, oldValue?: TValue) => void
onChange?(newValue: TValue, oldValue?: TValue): void
// By default category is id.split('.'). However, changing id to assign
// new category has poor backward compatibility. Use this field to overwrite
// default category from id.

View File

@@ -137,32 +137,40 @@ export const useExtensionService = () => {
}
}
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never
}[keyof T]
type RemoveLastAppParam<T> = T extends (
...args: [...infer Rest, ComfyApp]
) => infer R
? (...args: Rest) => R
: T
type ComfyExtensionParamsWithoutApp<T extends keyof ComfyExtension> =
RemoveLastAppParam<ComfyExtension[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 FunctionPropertyNames<ComfyExtension>>(
const invokeExtensions = <T extends KnownExtensionMethods>(
method: T,
...args: Parameters<ComfyExtensionParamsWithoutApp<T>>
) => {
const results: ReturnType<ComfyExtension[T]>[] = []
const results: ReturnType<ComfyExtensionMethod<T>>[] = []
for (const ext of extensionStore.enabledExtensions) {
if (method in ext) {
try {
results.push(ext[method](...args, app))
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}'`,
@@ -183,9 +191,7 @@ export const useExtensionService = () => {
* @param {...unknown} args Any arguments to pass to the callback
* @returns
*/
const invokeExtensionsAsync = async <
T extends FunctionPropertyNames<ComfyExtension>
>(
const invokeExtensionsAsync = async <T extends KnownExtensionMethods>(
method: T,
...args: Parameters<ComfyExtensionParamsWithoutApp<T>>
) => {
@@ -193,12 +199,17 @@ export const useExtensionService = () => {
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)
}
const result = await ext[method](...args, app)
const result = await fn.call(ext, ...args, app)
// Clear current extension after setup
if (method === 'setup') {

View File

@@ -144,7 +144,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
if (existingOutput && outputs) {
for (const k in outputs) {
const existingValue = existingOutput[k]
const newValue = (outputs as Record<NodeLocatorId, any>)[k]
const newValue = (outputs as Record<NodeLocatorId, unknown>)[k]
if (Array.isArray(existingValue) && Array.isArray(newValue)) {
existingOutput[k] = existingValue.concat(newValue)

View File

@@ -636,11 +636,11 @@ describe('useModelToNodeStore', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
expect(modelToNodeStore.getNodeProvider(null as any)).toBeUndefined()
expect(modelToNodeStore.getNodeProvider(undefined as any)).toBeUndefined()
expect(modelToNodeStore.getNodeProvider(123 as any)).toBeUndefined()
expect(modelToNodeStore.getAllNodeProviders(null as any)).toEqual([])
expect(modelToNodeStore.getAllNodeProviders(undefined as any)).toEqual([])
expect(modelToNodeStore.getNodeProvider(null)).toBeUndefined()
expect(modelToNodeStore.getNodeProvider(undefined)).toBeUndefined()
expect(modelToNodeStore.getNodeProvider(123)).toBeUndefined()
expect(modelToNodeStore.getAllNodeProviders(null)).toEqual([])
expect(modelToNodeStore.getAllNodeProviders(undefined)).toEqual([])
})
})
})

View File

@@ -104,7 +104,8 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
* @param modelType The name of the model type to get the node provider for.
* @returns The node provider for the given model type name.
*/
function getNodeProvider(modelType: string): ModelNodeProvider | undefined {
function getNodeProvider(modelType: unknown): ModelNodeProvider | undefined {
if (typeof modelType !== 'string') return undefined
registerDefaults()
return findProvidersWithFallback(modelType)?.[0]
}
@@ -115,7 +116,8 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
* @param modelType The name of the model type to get the node providers for.
* @returns The list of all valid node providers for the given model type name.
*/
function getAllNodeProviders(modelType: string): ModelNodeProvider[] {
function getAllNodeProviders(modelType: unknown): ModelNodeProvider[] {
if (typeof modelType !== 'string') return []
registerDefaults()
return findProvidersWithFallback(modelType) ?? []
}

View File

@@ -31,6 +31,13 @@ enum TaskItemDisplayStatus {
Cancelled = 'Cancelled'
}
interface ResultItemInit extends ResultItem {
nodeId: NodeId
mediaType: string
format?: string
frame_rate?: number
}
export class ResultItemImpl {
filename: string
subfolder: string
@@ -44,7 +51,7 @@ export class ResultItemImpl {
format?: string
frame_rate?: number
constructor(obj: Record<string, any>) {
constructor(obj: ResultItemInit) {
this.filename = obj.filename ?? ''
this.subfolder = obj.subfolder ?? ''
this.type = obj.type ?? ''

View File

@@ -2,7 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import type { ServerConfig } from '@/constants/serverConfig'
import type { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
import type { FormItem } from '@/platform/settings/types'
import { useServerConfigStore } from '@/stores/serverConfigStore'
@@ -28,7 +28,7 @@ describe('useServerConfigStore', () => {
})
it('should load server configs with default values', () => {
const configs: ServerConfig<any>[] = [
const configs: ServerConfig<ServerConfigValue>[] = [
{
...dummyFormItem,
id: 'test.config1',
@@ -50,7 +50,7 @@ describe('useServerConfigStore', () => {
})
it('should load server configs with provided values', () => {
const configs: ServerConfig<any>[] = [
const configs: ServerConfig<ServerConfigValue>[] = [
{
...dummyFormItem,
id: 'test.config1',
@@ -68,7 +68,7 @@ describe('useServerConfigStore', () => {
})
it('should organize configs by category', () => {
const configs: ServerConfig<any>[] = [
const configs: ServerConfig<ServerConfigValue>[] = [
{
...dummyFormItem,
id: 'test.config1',
@@ -97,7 +97,7 @@ describe('useServerConfigStore', () => {
})
it('should generate server config values excluding defaults', () => {
const configs: ServerConfig<any>[] = [
const configs: ServerConfig<ServerConfigValue>[] = [
{
...dummyFormItem,
id: 'test.config1',
@@ -121,12 +121,12 @@ describe('useServerConfigStore', () => {
})
it('should generate launch arguments with custom getValue function', () => {
const configs: ServerConfig<any>[] = [
const configs: ServerConfig<ServerConfigValue>[] = [
{
...dummyFormItem,
id: 'test.config1',
defaultValue: 'default1',
getValue: (value: string) => ({ customArg: value })
getValue: (value: ServerConfigValue) => ({ customArg: value })
},
{
...dummyFormItem,
@@ -146,7 +146,7 @@ describe('useServerConfigStore', () => {
})
it('should not include default values in launch arguments', () => {
const configs: ServerConfig<any>[] = [
const configs: ServerConfig<ServerConfigValue>[] = [
{
...dummyFormItem,
id: 'test.config1',
@@ -170,7 +170,7 @@ describe('useServerConfigStore', () => {
})
it('should not include nullish values in launch arguments', () => {
const configs: ServerConfig<any>[] = [
const configs: ServerConfig<ServerConfigValue>[] = [
{ ...dummyFormItem, id: 'test.config1', defaultValue: 'default1' },
{ ...dummyFormItem, id: 'test.config2', defaultValue: 'default2' },
{ ...dummyFormItem, id: 'test.config3', defaultValue: 'default3' },

View File

@@ -259,5 +259,5 @@ export interface ComfyExtension {
*/
onAuthUserLogout?(): Promise<void> | void
[key: string]: any
[key: string]: unknown
}

View File

@@ -7,8 +7,8 @@ export interface TreeNode extends PrimeVueTreeNode {
children?: this[]
}
export interface TreeExplorerNode<T = any> extends TreeNode {
data?: T
export interface TreeExplorerNode<T = unknown> extends TreeNode {
readonly data?: T
children?: this[]
icon?: string
/**
@@ -46,7 +46,7 @@ export interface TreeExplorerNode<T = any> extends TreeNode {
/** Function to handle dropping a node */
handleDrop?: (
this: TreeExplorerNode<T>,
data: TreeExplorerDragAndDropData
data: TreeExplorerDragAndDropData<T>
) => void | Promise<void>
/** Function to handle clicking a node */
handleClick?: (
@@ -58,10 +58,12 @@ export interface TreeExplorerNode<T = any> extends TreeNode {
/** Extra context menu items */
contextMenuItems?:
| MenuItem[]
| ((targetNode: RenderedTreeExplorerNode) => MenuItem[])
| ((targetNode: RenderedTreeExplorerNode<T>) => MenuItem[])
}
export interface RenderedTreeExplorerNode<T = any> extends TreeExplorerNode<T> {
export interface RenderedTreeExplorerNode<
T = unknown
> extends TreeExplorerNode<T> {
children?: this[]
icon: string
type: 'folder' | 'node'
@@ -73,7 +75,7 @@ export interface RenderedTreeExplorerNode<T = any> extends TreeExplorerNode<T> {
isEditingLabel?: boolean
}
export type TreeExplorerDragAndDropData<T = any> = {
export type TreeExplorerDragAndDropData<T = unknown> = {
type: 'tree-explorer-node'
data: RenderedTreeExplorerNode<T>
}

View File

@@ -50,6 +50,7 @@ import { useCoreCommands } from '@/composables/useCoreCommands'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useProgressFavicon } from '@/composables/useProgressFavicon'
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
import type { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
import { i18n, loadLocale } from '@/i18n'
import ModelImportProgressDialog from '@/platform/assets/components/ModelImportProgressDialog.vue'
import { isCloud, isDesktop } from '@/platform/distribution/types'
@@ -344,7 +345,7 @@ const onGraphReady = () => {
// Load server config
wrapWithErrorHandling(useServerConfigStore().loadServerConfig)(
SERVER_CONFIG_ITEMS,
SERVER_CONFIG_ITEMS as ServerConfig<ServerConfigValue>[],
settingStore.get('Comfy.Server.ServerConfigValues')
)

View File

@@ -6,7 +6,23 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, ref } from 'vue'
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import type {
ConflictDetail,
ConflictDetectionResult
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
// Type for component VM
interface NodeConflictDialogVM {
importFailedExpanded: boolean
conflictsExpanded: boolean
extensionsExpanded: boolean
allConflictDetails: ConflictDetail[]
importFailedConflicts: string[]
}
function getVM(wrapper: ReturnType<typeof mount>): NodeConflictDialogVM {
return wrapper.vm as Partial<NodeConflictDialogVM> as NodeConflictDialogVM
}
// Mock getConflictMessage utility
vi.mock('@/utils/conflictMessageUtil', () => ({
@@ -288,25 +304,28 @@ describe('NodeConflictDialogContent', () => {
await importFailedHeader.trigger('click')
// Verify import failed panel is open
expect((wrapper.vm as any).importFailedExpanded).toBe(true)
expect((wrapper.vm as any).conflictsExpanded).toBe(false)
expect((wrapper.vm as any).extensionsExpanded).toBe(false)
const vm1 = getVM(wrapper)
expect(vm1.importFailedExpanded).toBe(true)
expect(vm1.conflictsExpanded).toBe(false)
expect(vm1.extensionsExpanded).toBe(false)
// Open conflicts panel
await conflictsHeader.trigger('click')
// Verify conflicts panel is open and others are closed
expect((wrapper.vm as any).importFailedExpanded).toBe(false)
expect((wrapper.vm as any).conflictsExpanded).toBe(true)
expect((wrapper.vm as any).extensionsExpanded).toBe(false)
const vm2 = getVM(wrapper)
expect(vm2.importFailedExpanded).toBe(false)
expect(vm2.conflictsExpanded).toBe(true)
expect(vm2.extensionsExpanded).toBe(false)
// Open extensions panel
await extensionsHeader.trigger('click')
// Verify extensions panel is open and others are closed
expect((wrapper.vm as any).importFailedExpanded).toBe(false)
expect((wrapper.vm as any).conflictsExpanded).toBe(false)
expect((wrapper.vm as any).extensionsExpanded).toBe(true)
const vm3 = getVM(wrapper)
expect(vm3.importFailedExpanded).toBe(false)
expect(vm3.conflictsExpanded).toBe(false)
expect(vm3.extensionsExpanded).toBe(true)
})
})
@@ -451,10 +470,12 @@ describe('NodeConflictDialogContent', () => {
const wrapper = createWrapper()
// Verify that import_failed conflicts are filtered out from main conflicts
const vm = wrapper.vm as any
const vm = getVM(wrapper)
expect(vm.allConflictDetails).toHaveLength(3) // Should not include import_failed
expect(
vm.allConflictDetails.every((c: any) => c.type !== 'import_failed')
vm.allConflictDetails.every(
(c: ConflictDetail) => c.type !== 'import_failed'
)
).toBe(true)
})
@@ -463,7 +484,7 @@ describe('NodeConflictDialogContent', () => {
const wrapper = createWrapper()
// Verify that only import_failed packages are extracted
const vm = wrapper.vm as any
const vm = getVM(wrapper)
expect(vm.importFailedConflicts).toHaveLength(1)
expect(vm.importFailedConflicts[0]).toBe('Test Package 3')
})

View File

@@ -75,7 +75,7 @@ describe('PackVersionBadge', () => {
const mountComponent = ({
props = {}
}: Record<string, any> = {}): VueWrapper => {
}: { props?: Record<string, unknown> } = {}): VueWrapper => {
const i18n = createI18n({
legacy: false,
locale: 'en',

View File

@@ -17,6 +17,14 @@ import enMessages from '@/locales/en/main.json' with { type: 'json' }
import PackVersionSelectorPopover from './PackVersionSelectorPopover.vue'
interface PackVersionSelectorVM {
getVersionCompatibility: (version: string) => unknown
}
function getVM(wrapper: VueWrapper): PackVersionSelectorVM {
return wrapper.vm as Partial<PackVersionSelectorVM> as PackVersionSelectorVM
}
// Default mock versions for reference
const defaultMockVersions = [
{
@@ -106,7 +114,7 @@ describe('PackVersionSelectorPopover', () => {
const mountComponent = ({
props = {}
}: Record<string, any> = {}): VueWrapper => {
}: { props?: Record<string, unknown> } = {}): VueWrapper => {
const i18n = createI18n({
legacy: false,
locale: 'en',
@@ -481,7 +489,7 @@ describe('PackVersionSelectorPopover', () => {
mockCheckNodeCompatibility.mockClear()
// Trigger compatibility check by accessing getVersionCompatibility
const vm = wrapper.vm as any
const vm = getVM(wrapper)
vm.getVersionCompatibility('1.0.0')
// Verify that checkNodeCompatibility was called with correct data
@@ -569,7 +577,7 @@ describe('PackVersionSelectorPopover', () => {
})
await waitForPromises()
const vm = wrapper.vm as any
const vm = getVM(wrapper)
// Clear previous calls from component mounting/rendering
mockCheckNodeCompatibility.mockClear()

View File

@@ -17,7 +17,7 @@ vi.mock('es-toolkit/compat', async () => {
const actual = await vi.importActual('es-toolkit/compat')
return {
...actual,
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
debounce: <T extends (...args: unknown[]) => unknown>(fn: T) => fn
}
})
@@ -61,7 +61,10 @@ describe('PackEnableToggle', () => {
const mountComponent = ({
props = {},
installedPacks = {}
}: Record<string, any> = {}): VueWrapper => {
}: {
props?: Record<string, unknown>
installedPacks?: Record<string, unknown>
} = {}): VueWrapper => {
const i18n = createI18n({
legacy: false,
locale: 'en',
@@ -73,7 +76,9 @@ describe('PackEnableToggle', () => {
enablePack: mockEnablePack,
disablePack: mockDisablePack,
installedPacks
} as any)
} as Partial<ReturnType<typeof useComfyManagerStore>> as ReturnType<
typeof useComfyManagerStore
>)
return mount(PackEnableToggle, {
props: {