mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
## Summary
This PR removes `any` types from widgets, services, stores, and test
files, replacing them with proper TypeScript types.
### Key Changes
#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across widgets and services
- Added proper type imports (TgpuRoot, Point, StyleValue, etc.)
- Created typed interfaces (NumericWidgetOptions, TestWindow,
ImportFailureDetail, etc.)
- Fixed function return types to be non-nullable where appropriate
- Added type guards and null checks instead of non-null assertions
- Used `ComponentProps` from vue-component-type-helpers for component
testing
#### Widget System
- Added index signature to IWidgetOptions for Record compatibility
- Centralized disabled logic in WidgetInputNumberInput
- Moved template type assertions to computed properties
- Fixed ComboWidget getOptionLabel type assertions
- Improved remote widget type handling with runtime checks
#### Services & Stores
- Fixed getOrCreateViewer to return non-nullable values
- Updated addNodeOnGraph to use specific options type `{ pos?: Point }`
- Added proper type assertions for settings store retrieval
- Fixed executionIdToCurrentId return type (string | undefined)
#### Test Infrastructure
- Exported GraphOrSubgraph from litegraph barrel to avoid circular
dependencies
- Updated test fixtures with proper TypeScript types (TestInfo,
LGraphNode)
- Replaced loose Record types with ComponentProps in tests
- Added proper error handling in WebSocket fixture
#### Code Organization
- Created shared i18n-types module for locale data types
- Made ImportFailureDetail non-exported (internal use only)
- Added @public JSDoc tag to ElectronWindow type
- Fixed console.log usage in scripts to use allowed methods
### Files Changed
**Widgets & Components:**
-
src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
- src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.ts
- src/lib/litegraph/src/widgets/ComboWidget.ts
- src/lib/litegraph/src/types/widgets.ts
- src/components/common/LazyImage.vue
- src/components/load3d/Load3dViewerContent.vue
**Services & Stores:**
- src/services/litegraphService.ts
- src/services/load3dService.ts
- src/services/colorPaletteService.ts
- src/stores/maskEditorStore.ts
- src/stores/nodeDefStore.ts
- src/platform/settings/settingStore.ts
- src/platform/workflow/management/stores/workflowStore.ts
**Composables & Utils:**
- src/composables/node/useWatchWidget.ts
- src/composables/useCanvasDrop.ts
- src/utils/widgetPropFilter.ts
- src/utils/queueDisplay.ts
- src/utils/envUtil.ts
**Test Files:**
- browser_tests/fixtures/ComfyPage.ts
- browser_tests/fixtures/ws.ts
- browser_tests/tests/actionbar.spec.ts
-
src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts
- src/lib/litegraph/src/subgraph/subgraphUtils.test.ts
- src/components/rightSidePanel/shared.test.ts
- src/platform/cloud/subscription/composables/useSubscription.test.ts
-
src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts
**Scripts & Types:**
- scripts/i18n-types.ts (new shared module)
- scripts/diff-i18n.ts
- scripts/check-unused-i18n-keys.ts
- src/workbench/extensions/manager/types/conflictDetectionTypes.ts
- src/types/algoliaTypes.ts
- src/types/simplifiedWidget.ts
**Infrastructure:**
- src/lib/litegraph/src/litegraph.ts (added GraphOrSubgraph export)
- src/lib/litegraph/src/infrastructure/CustomEventTarget.ts
- src/platform/assets/services/assetService.ts
**Stories:**
- apps/desktop-ui/src/views/InstallView.stories.ts
- src/components/queue/job/JobDetailsPopover.stories.ts
**Extension Manager:**
- src/workbench/extensions/manager/composables/useConflictDetection.ts
- src/workbench/extensions/manager/composables/useManagerQueue.ts
- src/workbench/extensions/manager/services/comfyManagerService.ts
- src/workbench/extensions/manager/utils/conflictMessageUtil.ts
### Testing
- [x] All TypeScript type checking passes (`pnpm typecheck`)
- [x] ESLint passes without errors (`pnpm lint`)
- [x] Format checks pass (`pnpm format:check`)
- [x] Knip (unused exports) passes (`pnpm knip`)
- [x] Pre-commit and pre-push hooks pass
Part of the "Road to No Explicit Any" initiative.
### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496
- Part 9: #8498
- Part 10: #8499
---------
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
190 lines
5.7 KiB
TypeScript
Executable File
190 lines
5.7 KiB
TypeScript
Executable File
#!/usr/bin/env tsx
|
||
import { execSync } from 'child_process'
|
||
import * as fs from 'fs'
|
||
import { globSync } from 'glob'
|
||
import type { LocaleData } from './i18n-types'
|
||
|
||
// Configuration
|
||
const SOURCE_PATTERNS = ['src/**/*.{js,ts,vue}', '!src/locales/**/*']
|
||
const IGNORE_PATTERNS = [
|
||
// Keys that might be dynamically constructed
|
||
/^commands\./, // Command definitions are loaded dynamically
|
||
/^settings\..*\.options\./, // Setting options are rendered dynamically
|
||
/^nodeDefs\./, // Node definitions are loaded from backend
|
||
/^templateWorkflows\./, // Template workflows are loaded dynamically
|
||
/^dataTypes\./, // Data types might be referenced dynamically
|
||
/^contextMenu\./, // Context menu items might be dynamic
|
||
/^color\./, // Color names might be used dynamically
|
||
// Auto-generated categories from collect-i18n-general.ts
|
||
/^menuLabels\./, // Menu labels generated from command labels
|
||
/^settingsCategories\./, // Settings categories generated from setting definitions
|
||
/^serverConfigItems\./, // Server config items generated from SERVER_CONFIG_ITEMS
|
||
/^serverConfigCategories\./, // Server config categories generated from config categories
|
||
/^nodeCategories\./, // Node categories generated from node definitions
|
||
// Setting option values that are dynamically generated
|
||
/\.options\./ // All setting options are rendered dynamically
|
||
]
|
||
|
||
// Get list of staged locale files
|
||
function getStagedLocaleFiles(): string[] {
|
||
try {
|
||
const output = execSync('git diff --cached --name-only --diff-filter=AM', {
|
||
encoding: 'utf-8'
|
||
})
|
||
return output
|
||
.split('\n')
|
||
.filter(
|
||
(file) => file.startsWith('src/locales/') && file.endsWith('.json')
|
||
)
|
||
} catch {
|
||
return []
|
||
}
|
||
}
|
||
|
||
// Extract all keys from a nested object
|
||
function extractKeys(obj: LocaleData, prefix = ''): string[] {
|
||
const keys: string[] = []
|
||
|
||
for (const [key, value] of Object.entries(obj)) {
|
||
const fullKey = prefix ? `${prefix}.${key}` : key
|
||
|
||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||
keys.push(...extractKeys(value, fullKey))
|
||
} else {
|
||
keys.push(fullKey)
|
||
}
|
||
}
|
||
|
||
return keys
|
||
}
|
||
|
||
// Get new keys added in staged files
|
||
function getNewKeysFromStagedFiles(stagedFiles: string[]): Set<string> {
|
||
const newKeys = new Set<string>()
|
||
|
||
for (const file of stagedFiles) {
|
||
try {
|
||
// Get the staged content
|
||
const stagedContent = execSync(`git show :${file}`, { encoding: 'utf-8' })
|
||
const stagedData: LocaleData = JSON.parse(stagedContent)
|
||
const stagedKeys = new Set(extractKeys(stagedData))
|
||
|
||
// Get the current HEAD content (if file exists)
|
||
let headKeys = new Set<string>()
|
||
try {
|
||
const headContent = execSync(`git show HEAD:${file}`, {
|
||
encoding: 'utf-8'
|
||
})
|
||
const headData: LocaleData = JSON.parse(headContent)
|
||
headKeys = new Set(extractKeys(headData))
|
||
} catch {
|
||
// File is new, all keys are new
|
||
}
|
||
|
||
// Find keys that are in staged but not in HEAD
|
||
stagedKeys.forEach((key) => {
|
||
if (!headKeys.has(key)) {
|
||
newKeys.add(key)
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error(`Error processing ${file}:`, error)
|
||
}
|
||
}
|
||
|
||
return newKeys
|
||
}
|
||
|
||
// Check if a key should be ignored
|
||
function shouldIgnoreKey(key: string): boolean {
|
||
return IGNORE_PATTERNS.some((pattern) => pattern.test(key))
|
||
}
|
||
|
||
// Search for key usage in source files
|
||
function isKeyUsed(key: string, sourceFiles: string[]): boolean {
|
||
// Escape special regex characters
|
||
const escapeRegex = (str: string) =>
|
||
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||
const escapedKey = escapeRegex(key)
|
||
const lastPart = key.split('.').pop()
|
||
const escapedLastPart = lastPart ? escapeRegex(lastPart) : ''
|
||
|
||
// Common patterns for i18n key usage
|
||
const patterns = [
|
||
// Direct usage: $t('key'), t('key'), i18n.t('key')
|
||
new RegExp(`[t$]\\s*\\(\\s*['"\`]${escapedKey}['"\`]`, 'g'),
|
||
// With namespace: $t('g.key'), t('namespace.key')
|
||
new RegExp(`[t$]\\s*\\(\\s*['"\`][^'"]+\\.${escapedLastPart}['"\`]`, 'g'),
|
||
// Dynamic keys might reference parts of the key
|
||
new RegExp(`['"\`]${escapedKey}['"\`]`, 'g')
|
||
]
|
||
|
||
for (const file of sourceFiles) {
|
||
const content = fs.readFileSync(file, 'utf-8')
|
||
|
||
for (const pattern of patterns) {
|
||
if (pattern.test(content)) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// Main function
|
||
async function checkNewUnusedKeys() {
|
||
const stagedLocaleFiles = getStagedLocaleFiles()
|
||
|
||
if (stagedLocaleFiles.length === 0) {
|
||
// No locale files staged, nothing to check
|
||
process.exit(0)
|
||
}
|
||
|
||
// Get all new keys from staged files
|
||
const newKeys = getNewKeysFromStagedFiles(stagedLocaleFiles)
|
||
|
||
if (newKeys.size === 0) {
|
||
// Silent success - no output needed
|
||
process.exit(0)
|
||
}
|
||
|
||
// Get all source files
|
||
const sourceFiles = globSync(SOURCE_PATTERNS)
|
||
|
||
// Check each new key
|
||
const unusedNewKeys: string[] = []
|
||
|
||
newKeys.forEach((key) => {
|
||
if (!shouldIgnoreKey(key) && !isKeyUsed(key, sourceFiles)) {
|
||
unusedNewKeys.push(key)
|
||
}
|
||
})
|
||
|
||
// Report results
|
||
if (unusedNewKeys.length > 0) {
|
||
console.warn('\n⚠️ Warning: Found unused NEW i18n keys:\n')
|
||
|
||
for (const key of unusedNewKeys.sort()) {
|
||
console.warn(` - ${key}`)
|
||
}
|
||
|
||
console.warn(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
|
||
console.warn(
|
||
'\nThese keys were added but are not used anywhere in the codebase.'
|
||
)
|
||
console.warn('Consider using them or removing them in a future update.')
|
||
|
||
// Changed from process.exit(1) to process.exit(0) for warning only
|
||
process.exit(0)
|
||
} else {
|
||
// Silent success - no output needed
|
||
}
|
||
}
|
||
|
||
// Run the check
|
||
checkNewUnusedKeys().catch((err) => {
|
||
console.error('Error checking unused keys:', err)
|
||
process.exit(1)
|
||
})
|