style: reformat codebase with oxfmt

This commit is contained in:
Alexander Brown
2026-01-19 16:12:59 -08:00
parent a64a9cac95
commit 677c33f4fe
174 changed files with 25355 additions and 4364 deletions

View File

@@ -17,19 +17,19 @@ sequenceDiagram
Frontend->>WebSocket: Connect
WebSocket-->>Frontend: Connection established
Note over Frontend: First message must be feature flags
Frontend->>WebSocket: Send client feature flags
WebSocket->>Backend: Receive feature flags
Backend->>FeatureFlags Module: Store client capabilities
Backend->>FeatureFlags Module: Get server features
FeatureFlags Module-->>Backend: Return server capabilities
Backend->>WebSocket: Send server feature flags
WebSocket-->>Frontend: Receive server features
Note over Frontend,Backend: Both sides now know each other's capabilities
Frontend->>Frontend: Store server features
Frontend->>Frontend: Components use useFeatureFlags()
```
@@ -44,15 +44,15 @@ graph TB
D[useFeatureFlags composable] --> B
E[Vue Components] --> D
end
subgraph Backend
F[feature_flags.py] --> G[SERVER_FEATURE_FLAGS]
H[server.py WebSocket] --> F
I[Feature Consumers] --> F
end
C <--> H
style A fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#f9f,stroke:#333,stroke-width:2px
style D fill:#9ff,stroke:#333,stroke-width:2px
@@ -98,19 +98,19 @@ classDiagram
+supports_feature(sockets_metadata, sid, feature_name) bool
+get_connection_feature(sockets_metadata, sid, feature_name, default) Any
}
class PromptServer {
-sockets_metadata: Dict
+websocket_handler()
+send()
}
class FeatureConsumer {
<<interface>>
+check_feature()
+use_feature()
}
PromptServer --> FeatureFlagsModule
FeatureConsumer --> FeatureFlagsModule
```
@@ -127,19 +127,19 @@ classDiagram
+serverSupportsFeature(name) boolean
+getServerFeature(name, default) T
}
class useFeatureFlags {
+serverSupports(name) boolean
+getServerFeature(name, default) T
+createServerFeatureFlag(name) ComputedRef
+extension: ExtensionFlags
}
class VueComponent {
<<component>>
+setup()
}
ComfyApi <-- useFeatureFlags
VueComponent --> useFeatureFlags
```
@@ -153,12 +153,13 @@ graph LR
A[Preview Generation] --> B{supports_preview_metadata?}
B -->|Yes| C[Send metadata with preview]
B -->|No| D[Send preview only]
C --> E[Enhanced preview with node info]
D --> F[Basic preview image]
```
**Backend Usage:**
```python
# Check if client supports preview metadata
if feature_flags.supports_feature(
@@ -189,13 +190,14 @@ graph TB
B --> C{File size OK?}
C -->|Yes| D[Upload file]
C -->|No| E[Show error]
F[Backend] --> G[Set from CLI args]
G --> H[Convert MB to bytes]
H --> I[Include in feature flags]
```
**Backend Configuration:**
```python
# In feature_flags.py
SERVER_FEATURE_FLAGS = {
@@ -205,6 +207,7 @@ SERVER_FEATURE_FLAGS = {
```
**Frontend Usage:**
```typescript
const { getServerFeature } = useFeatureFlags()
const maxUploadSize = getServerFeature('max_upload_size', 100 * 1024 * 1024) // Default 100MB
@@ -215,10 +218,11 @@ const maxUploadSize = getServerFeature('max_upload_size', 100 * 1024 * 1024) //
### Frontend Access Patterns
1. **Direct API access:**
```typescript
// Check boolean feature
if (api.serverSupportsFeature('supports_preview_metadata')) {
// Feature is supported
// Feature is supported
}
// Get feature value with default
@@ -226,21 +230,23 @@ const maxSize = api.getServerFeature('max_upload_size', 100 * 1024 * 1024)
```
2. **Using the composable (recommended for reactive components):**
```typescript
const { serverSupports, getServerFeature, extension } = useFeatureFlags()
// Check feature support
if (serverSupports('supports_preview_metadata')) {
// Use enhanced previews
// Use enhanced previews
}
// Use reactive convenience properties (automatically update if flags change)
if (extension.manager.supportsV4.value) {
// Use V4 manager API
// Use V4 manager API
}
```
3. **Reactive usage in templates:**
```vue
<template>
<div v-if="featureFlags.extension.manager.supportsV4">
@@ -262,12 +268,12 @@ const featureFlags = useFeatureFlags()
```python
# Check if a specific client supports a feature
if feature_flags.supports_feature(
sockets_metadata,
client_id,
sockets_metadata,
client_id,
"supports_preview_metadata"
):
# Client supports this feature
# Get feature value with default
max_size = feature_flags.get_connection_feature(
sockets_metadata,
@@ -282,6 +288,7 @@ max_size = feature_flags.get_connection_feature(
### Backend
1. **For server capabilities**, add to `SERVER_FEATURE_FLAGS` in `comfy_api/feature_flags.py`:
```python
SERVER_FEATURE_FLAGS = {
"supports_preview_metadata": True,
@@ -291,6 +298,7 @@ SERVER_FEATURE_FLAGS = {
```
2. **Use in your code:**
```python
if feature_flags.supports_feature(sockets_metadata, sid, "your_new_feature"):
# Feature-specific code
@@ -299,28 +307,34 @@ if feature_flags.supports_feature(sockets_metadata, sid, "your_new_feature"):
### Frontend
1. **For client capabilities**, add to `src/config/clientFeatureFlags.json`:
```json
{
"supports_preview_metadata": false,
"your_new_feature": true
"supports_preview_metadata": false,
"your_new_feature": true
}
```
2. **For extension features**, update the composable to add convenience accessors:
```typescript
// In useFeatureFlags.ts
const extension = {
manager: {
supportsV4: computed(() => getServerFeature('extension.manager.supports_v4', false))
},
yourExtension: {
supportsNewFeature: computed(() => getServerFeature('extension.yourExtension.supports_new_feature', false))
}
manager: {
supportsV4: computed(() =>
getServerFeature('extension.manager.supports_v4', false)
)
},
yourExtension: {
supportsNewFeature: computed(() =>
getServerFeature('extension.yourExtension.supports_new_feature', false)
)
}
}
return {
// ... existing returns
extension
// ... existing returns
extension
}
```
@@ -332,7 +346,7 @@ graph LR
A --> C[Only frontend supports]
A --> D[Only backend supports]
A --> E[Neither supports]
B --> F[Feature enabled]
C --> G[Feature disabled]
D --> H[Feature disabled]
@@ -340,6 +354,7 @@ graph LR
```
Test your feature flags with different combinations:
- Frontend with flag + Backend with flag = Feature works
- Frontend with flag + Backend without = Graceful degradation
- Frontend without + Backend with flag = No feature usage
@@ -350,13 +365,14 @@ Test your feature flags with different combinations:
```typescript
// In tests-ui/tests/api.featureFlags.test.ts
it('should handle preview metadata based on feature flag', () => {
// Mock server supports feature
api.serverFeatureFlags = { supports_preview_metadata: true }
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
// Mock server doesn't support feature
api.serverFeatureFlags = {}
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false)
})
// Mock server supports feature
api.serverFeatureFlags = { supports_preview_metadata: true }
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
// Mock server doesn't support feature
api.serverFeatureFlags = {}
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false)
})
```

View File

@@ -5,6 +5,7 @@
ComfyUI frontend uses a comprehensive settings system for user preferences with support for dynamic defaults, version-based rollouts, and environment-aware configuration.
### Settings Architecture
- Settings are defined as `SettingParams` in `src/constants/coreSettings.ts`
- Registered at app startup, loaded/saved via `useSettingStore` (Pinia)
- Persisted per user via backend `/settings` endpoint
@@ -45,6 +46,7 @@ await newUserService().initializeIfNewUser(settingStore)
## Dynamic and Environment-Based Defaults
### Computed Defaults
You can compute defaults dynamically using function defaults that access runtime context:
```typescript
@@ -65,14 +67,15 @@ You can compute defaults dynamically using function defaults that access runtime
```
### Version-Based Defaults
You can vary defaults by installed frontend version using `defaultsByInstallVersion`:
```typescript
// From src/stores/settingStore.ts:129-150
function getVersionedDefaultValue<K extends keyof Settings, TValue = Settings[K]>(
key: K,
param: SettingParams<TValue> | undefined
): TValue | null {
function getVersionedDefaultValue<
K extends keyof Settings,
TValue = Settings[K]
>(key: K, param: SettingParams<TValue> | undefined): TValue | null {
const defaultsByInstallVersion = param?.defaultsByInstallVersion
if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
const installedVersion = get('Comfy.InstalledVersion')
@@ -166,15 +169,13 @@ The initial installed version is captured for new users to ensure versioned defa
```typescript
// From src/services/newUserService.ts:49-53
await settingStore.set(
'Comfy.InstalledVersion',
__COMFYUI_FRONTEND_VERSION__
)
await settingStore.set('Comfy.InstalledVersion', __COMFYUI_FRONTEND_VERSION__)
```
## Practical Patterns for Environment-Based Defaults
### Dynamic Default Patterns
```typescript
// Device-based default
{
@@ -199,6 +200,7 @@ await settingStore.set(
```
### Version-Based Rollout Pattern
```typescript
{
id: 'Comfy.Example.NewFeature',
@@ -214,6 +216,7 @@ await settingStore.set(
## Settings Persistence and Access
### API Interaction
Values are stored per user via the backend. The store writes through API and falls back to defaults when not set:
```typescript
@@ -224,6 +227,7 @@ await api.storeSetting(key, newValue)
```
### Usage in Components
```typescript
const settingStore = useSettingStore()
@@ -234,7 +238,6 @@ const value = settingStore.get('Comfy.SomeSetting')
await settingStore.set('Comfy.SomeSetting', newValue)
```
## Advanced Settings Features
### Migration and Backward Compatibility
@@ -243,10 +246,7 @@ Settings support migration from deprecated values:
```typescript
// From src/stores/settingStore.ts:68-69, 172-175
const newValue = tryMigrateDeprecatedValue(
settingsById.value[key],
clonedValue
)
const newValue = tryMigrateDeprecatedValue(settingsById.value[key], clonedValue)
// Migration happens during addSetting for existing values:
if (settingValues.value[setting.id] !== undefined) {
@@ -263,8 +263,8 @@ Settings can define onChange callbacks that receive the setting definition, new
```typescript
// From src/stores/settingStore.ts:73, 177
onChange(settingsById.value[key], newValue, oldValue) // During set()
onChange(setting, get(setting.id), undefined) // During addSetting()
onChange(settingsById.value[key], newValue, oldValue) // During set()
onChange(setting, get(setting.id), undefined) // During addSetting()
```
### Settings UI and Categories
@@ -290,4 +290,4 @@ Settings are automatically grouped for UI based on their `category` or derived f
- **Settings**: User preferences with dynamic/versioned defaults, persisted per user
- **Environment Defaults**: Use function defaults to read runtime context (window, navigator, env)
- **Version Rollouts**: Use `defaultsByInstallVersion` for gradual feature releases
- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()`
- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()`

View File

@@ -11,11 +11,13 @@ Accepted
ComfyUI's frontend architecture currently depends on a forked version of litegraph.js maintained as a separate package (@comfyorg/litegraph). This separation has created several architectural and operational challenges:
**Architectural Issues:**
- The current split creates a distributed monolith where both packages handle rendering, user interactions, and data models without clear separation of responsibilities
- Both frontend and litegraph manipulate the same data structures, forcing tight coupling across the frontend's data model, views, and business logic
- The lack of clear boundaries prevents implementation of modern architectural patterns like MVC or event-sourcing
**Operational Issues:**
- ComfyUI is the only known user of the @comfyorg/litegraph fork
- Managing separate repositories significantly slows developer velocity due to coordination overhead
- Version mismatches between frontend and litegraph cause recurring issues
@@ -23,6 +25,7 @@ ComfyUI's frontend architecture currently depends on a forked version of litegra
**Future Requirements:**
The following planned features are blocked by the current architecture:
- Multiplayer collaboration requiring CRDT-based state management
- Cloud-based backend support
- Alternative rendering backends
@@ -34,6 +37,7 @@ The following planned features are blocked by the current architecture:
We will merge litegraph.js directly into the ComfyUI frontend repository using git subtree to preserve the complete commit history.
The merge will:
1. Move litegraph source to `src/lib/litegraph/`
2. Update all import paths from `@comfyorg/litegraph` to `@/lib/litegraph`
3. Remove the npm dependency on `@comfyorg/litegraph`
@@ -62,4 +66,4 @@ This integration is the first step toward restructuring the application along cl
- Git subtree was chosen over submodules to provide a cleaner developer experience
- The original litegraph repository will be archived after the merge
- Future litegraph improvements will be made directly in the frontend repository
- Future litegraph improvements will be made directly in the frontend repository

View File

@@ -13,7 +13,7 @@ Proposed
[Most of the context is in here](https://github.com/Comfy-Org/ComfyUI_frontend/issues/4661)
TL;DR: As we're merging more subprojects like litegraph, devtools, and soon a fork of PrimeVue,
a monorepo structure will help a lot with code sharing and organization.
a monorepo structure will help a lot with code sharing and organization.
For more information on Monorepos, check out [monorepo.tools](https://monorepo.tools/)
@@ -37,7 +37,7 @@ There's a [whole list here](https://monorepo.tools/#tools-review) if you're inte
- Adding new projects with shared dependencies becomes really easy
- Makes the process of forking and customizing projects more structured, if not strictly easier
- It *could* speed up the build and development process (not guaranteed)
- It _could_ speed up the build and development process (not guaranteed)
- It would let us cleanly organize and release packages like `comfyui-frontend-types`
### Negative
@@ -47,4 +47,4 @@ There's a [whole list here](https://monorepo.tools/#tools-review) if you're inte
<!-- ## Notes
Optional section for additional information, references, or clarifications. -->
Optional section for additional information, references, or clarifications. -->

View File

@@ -24,7 +24,7 @@ The existing system allows each node to directly mutate its position within Lite
5. **Inefficient Change Detection**: While LiteGraph provides some events, many operations require polling via changeTracker.ts. The current undo/redo system performs expensive diffs on every interaction rather than using reactive push/pull signals, creating performance bottlenecks and blocking efficient animations and viewport culling.
This represents a fundamental architectural limitation: diff-based systems scale O(n) with graph complexity (traverse entire structure to detect changes), while signal-based reactive systems scale O(1) with actual changes (data mutations automatically notify subscribers). Modern frameworks (Vue 3, Angular signals, SolidJS) have moved to reactive approaches for precisely this performance reason.
This represents a fundamental architectural limitation: diff-based systems scale O(n) with graph complexity (traverse entire structure to detect changes), while signal-based reactive systems scale O(1) with actual changes (data mutations automatically notify subscribers). Modern frameworks (Vue 3, Angular signals, SolidJS) have moved to reactive approaches for precisely this performance reason.
### Business Context
@@ -53,12 +53,14 @@ This provides single source of truth, predictable state updates, and natural sys
### Core Architecture
1. **Centralized Layout Store**: A Yjs CRDT maintains all spatial data in a single authoritative store:
```typescript
// Instead of: node.position = {x, y}
layoutStore.moveNode(nodeId, {x, y})
layoutStore.moveNode(nodeId, { x, y })
```
2. **Command Pattern**: All spatial mutations flow through explicit commands:
```
User Input → Commands → Layout Store → Observer Notifications → Renderers
```
@@ -74,12 +76,14 @@ This provides single source of truth, predictable state updates, and natural sys
### Implementation Strategy
**Phase 1: Parallel System**
- Build CRDT layout store alongside existing system
- Layout store initially mirrors LiteGraph changes via observers
- Gradually migrate user interactions to use command interface
- Maintain full backward compatibility
**Phase 2: Inversion of Control**
- CRDT store becomes single source of truth
- LiteGraph receives position updates via reactive subscriptions
- Enable alternative renderers and advanced features
@@ -89,17 +93,20 @@ This provides single source of truth, predictable state updates, and natural sys
This combination provides both architectural and technical benefits:
**Centralized State Benefits:**
- **Single Source of Truth**: All layout data managed in one place, eliminating conflicts
- **System Decoupling**: Rendering, interaction, and layout systems operate independently
- **Predictable Updates**: Clear data flow makes debugging and testing easier
- **Extensibility**: Easy to add new layout behaviors without modifying existing systems
**CRDT Benefits:**
- **Conflict Resolution**: Automatic merging eliminates position conflicts between systems
- **Collaboration-Ready**: Built-in support for multi-user editing
- **Eventual Consistency**: Guaranteed convergence to same state across all clients
**Yjs-Specific Benefits:**
- **Event-Driven**: Native observer pattern removes need for polling
- **Selective Updates**: Only changed nodes trigger system updates
- **Fine-Grained Changes**: Efficient delta synchronization
@@ -109,7 +116,7 @@ This combination provides both architectural and technical benefits:
### Positive
- **Eliminates Polling**: Observer pattern removes O(n) graph traversals, improving performance
- **System Modularity**: Independent systems can be developed, tested, and optimized separately
- **System Modularity**: Independent systems can be developed, tested, and optimized separately
- **Renderer Flexibility**: Easy to add WebGL, DOM accessibility, or hybrid rendering systems
- **Rich Interactions**: Command pattern enables robust undo/redo, macros, and interaction history
- **Collaboration-Ready**: CRDT foundation enables real-time multi-user editing
@@ -140,9 +147,10 @@ This centralized state + CRDT architecture follows patterns from modern collabor
**CRDT in Collaboration**: Tools like Figma, Linear, and Notion use similar approaches for real-time collaboration, demonstrating the effectiveness of separating authoritative data from presentation logic.
**Future Capabilities**: This foundation enables advanced features that would be difficult with the current architecture:
- Macro recording and workflow automation
- Programmatic layout optimization and constraints
- API-driven workflow construction
- API-driven workflow construction
- Multiple simultaneous renderers (canvas + accessibility DOM)
- Real-time collaborative editing
- Advanced spatial features (physics, animations, auto-layout)

View File

@@ -11,22 +11,27 @@ Rejected
ComfyUI's frontend requires modifications to PrimeVue components that cannot be achieved through the library's customization APIs. Two specific technical incompatibilities have been identified with the transform-based canvas architecture:
**Screen Coordinate Hit-Testing Conflicts:**
- PrimeVue components use `getBoundingClientRect()` for screen coordinate calculations that don't account for CSS transforms
- The Slider component directly uses raw `pageX/pageY` coordinates ([lines 102-103](https://github.com/primefaces/primevue/blob/master/packages/primevue/src/slider/Slider.vue#L102-L103)) without transform-aware positioning
- This breaks interaction in transformed coordinate spaces where screen coordinates don't match logical element positions
**Virtual Canvas Scroll Interference:**
- LiteGraph's infinite canvas uses scroll coordinates semantically for graph navigation via the `DragAndScale` coordinate system
- PrimeVue overlay components automatically trigger `scrollIntoView` behavior which interferes with this virtual positioning
- This issue is documented in [PrimeVue discussion #4270](https://github.com/orgs/primefaces/discussions/4270) where the feature request was made to disable this behavior
**Historical Overlay Issues:**
- Previous z-index positioning conflicts required manual workarounds (commit `6d4eafb0`) where PrimeVue Dialog components needed `autoZIndex: false` and custom mask styling, later resolved by removing PrimeVue's automatic z-index management entirely
**Minimal Update Overhead:**
- Analysis of git history shows only 2 PrimeVue version updates in 2+ years, indicating that upstream sync overhead is negligible for this project
**Future Interaction System Requirements:**
- The ongoing canvas architecture evolution will require more granular control over component interaction and event handling as the transform-based system matures
- Predictable need for additional component modifications beyond current identified issues

View File

@@ -8,13 +8,13 @@ An Architecture Decision Record captures an important architectural decision mad
## ADR Index
| ADR | Title | Status | Date |
|-----|-------|--------|------|
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
| ADR | Title | Status | Date |
| --------------------------------------------------- | ---------------------------------------- | -------- | ---------- |
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
## Creating a New ADR

View File

@@ -11,7 +11,6 @@ Extensions are the primary way to add functionality to ComfyUI. They can be cust
- How extensions load (backend vs frontend)
- Why extensions don't work in dev server
- Development workarounds and best practices
- **[Core Extensions Reference](./core.md)** - Detailed reference for core extensions:
- Complete list of all core extensions
- Extension architecture principles
@@ -42,4 +41,4 @@ Extensions are the primary way to add functionality to ComfyUI. They can be cust
- Check the [Development Guide](./development.md) for common issues
- Review [Core Extensions](./core.md) for examples
- Visit the [ComfyUI Discord](https://discord.com/invite/comfyorg) for community support
- Visit the [ComfyUI Discord](https://discord.com/invite/comfyorg) for community support

View File

@@ -29,56 +29,58 @@ The following table lists ALL core extensions in the system as of 2025-01-30:
### Main Extensions
| Extension | Description | Category |
|-----------|-------------|----------|
| clipspace.ts | Implements the Clipspace feature for temporary image storage | Image |
| contextMenuFilter.ts | Provides context menu filtering capabilities | UI |
| dynamicPrompts.ts | Provides dynamic prompt generation capabilities | Prompts |
| editAttention.ts | Implements attention editing functionality | Text |
| electronAdapter.ts | Adapts functionality for Electron environment | Platform |
| groupNode.ts | Implements the group node functionality to organize workflows | Graph |
| groupNodeManage.ts | Provides group node management operations | Graph |
| groupOptions.ts | Handles group node configuration options | Graph |
| index.ts | Main extension registration and coordination | Core |
| load3d.ts | Supports 3D model loading and visualization | 3D |
| maskeditor.ts | Implements the mask editor for image masking operations | Image |
| nodeTemplates.ts | Provides node template functionality | Templates |
| noteNode.ts | Adds note nodes for documentation within workflows | Graph |
| previewAny.ts | Universal preview functionality for various data types | Preview |
| rerouteNode.ts | Implements reroute nodes for cleaner workflow connections | Graph |
| saveImageExtraOutput.ts | Handles additional image output saving | Image |
| saveMesh.ts | Implements 3D mesh saving functionality | 3D |
| simpleTouchSupport.ts | Provides basic touch interaction support | Input |
| slotDefaults.ts | Manages default values for node slots | Nodes |
| uploadAudio.ts | Handles audio file upload functionality | Audio |
| uploadImage.ts | Handles image upload functionality | Image |
| webcamCapture.ts | Provides webcam capture capabilities | Media |
| widgetInputs.ts | Implements various widget input types | Widgets |
| Extension | Description | Category |
| ----------------------- | ------------------------------------------------------------- | --------- |
| clipspace.ts | Implements the Clipspace feature for temporary image storage | Image |
| contextMenuFilter.ts | Provides context menu filtering capabilities | UI |
| dynamicPrompts.ts | Provides dynamic prompt generation capabilities | Prompts |
| editAttention.ts | Implements attention editing functionality | Text |
| electronAdapter.ts | Adapts functionality for Electron environment | Platform |
| groupNode.ts | Implements the group node functionality to organize workflows | Graph |
| groupNodeManage.ts | Provides group node management operations | Graph |
| groupOptions.ts | Handles group node configuration options | Graph |
| index.ts | Main extension registration and coordination | Core |
| load3d.ts | Supports 3D model loading and visualization | 3D |
| maskeditor.ts | Implements the mask editor for image masking operations | Image |
| nodeTemplates.ts | Provides node template functionality | Templates |
| noteNode.ts | Adds note nodes for documentation within workflows | Graph |
| previewAny.ts | Universal preview functionality for various data types | Preview |
| rerouteNode.ts | Implements reroute nodes for cleaner workflow connections | Graph |
| saveImageExtraOutput.ts | Handles additional image output saving | Image |
| saveMesh.ts | Implements 3D mesh saving functionality | 3D |
| simpleTouchSupport.ts | Provides basic touch interaction support | Input |
| slotDefaults.ts | Manages default values for node slots | Nodes |
| uploadAudio.ts | Handles audio file upload functionality | Audio |
| uploadImage.ts | Handles image upload functionality | Image |
| webcamCapture.ts | Provides webcam capture capabilities | Media |
| widgetInputs.ts | Implements various widget input types | Widgets |
### Conditional Lines Subdirectory
Located in `extensions/core/load3d/conditional-lines/`:
| File | Description |
|------|-------------|
| ColoredShadowMaterial.js | Material for colored shadow rendering |
| File | Description |
| --------------------------- | --------------------------------------- |
| ColoredShadowMaterial.js | Material for colored shadow rendering |
| ConditionalEdgesGeometry.js | Geometry for conditional edge rendering |
| ConditionalEdgesShader.js | Shader for conditional edges |
| OutsideEdgesGeometry.js | Geometry for outside edge detection |
| ConditionalEdgesShader.js | Shader for conditional edges |
| OutsideEdgesGeometry.js | Geometry for outside edge detection |
### Lines2 Subdirectory
### Lines2 Subdirectory
Located in `extensions/core/load3d/conditional-lines/Lines2/`:
| File | Description |
|------|-------------|
| ConditionalLineMaterial.js | Material for conditional line rendering |
| ConditionalLineSegmentsGeometry.js | Geometry for conditional line segments |
| File | Description |
| ---------------------------------- | --------------------------------------- |
| ConditionalLineMaterial.js | Material for conditional line rendering |
| ConditionalLineSegmentsGeometry.js | Geometry for conditional line segments |
### ThreeJS Override Subdirectory
Located in `extensions/core/load3d/threejsOverride/`:
| File | Description |
|------|-------------|
| File | Description |
| -------------------- | --------------------------------------------- |
| OverrideMTLLoader.js | Custom MTL loader with enhanced functionality |
## Extension Development
@@ -97,19 +99,19 @@ Extensions are registered using the `app.registerExtension()` method:
```javascript
app.registerExtension({
name: "MyExtension",
name: 'MyExtension',
// Hook implementations
async init() {
// Implementation
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Implementation
}
// Other hooks as needed
});
})
```
## Extension Hooks
@@ -151,18 +153,18 @@ nodeCreated
### Key Hooks
| Hook | Description |
|------|-------------|
| `init` | Called after canvas creation but before nodes are added |
| `setup` | Called after the application is fully set up and running |
| `addCustomNodeDefs` | Called before nodes are registered with the graph |
| `getCustomWidgets` | Allows extensions to add custom widgets |
| `beforeRegisterNodeDef` | Allows extensions to modify nodes before registration |
| `registerCustomNodes` | Allows extensions to register additional nodes |
| `loadedGraphNode` | Called when a node is reloaded onto the graph |
| `nodeCreated` | Called after a node's constructor |
| `beforeConfigureGraph` | Called before a graph is configured |
| `afterConfigureGraph` | Called after a graph is configured |
| Hook | Description |
| ----------------------------- | ---------------------------------------------------------- |
| `init` | Called after canvas creation but before nodes are added |
| `setup` | Called after the application is fully set up and running |
| `addCustomNodeDefs` | Called before nodes are registered with the graph |
| `getCustomWidgets` | Allows extensions to add custom widgets |
| `beforeRegisterNodeDef` | Allows extensions to modify nodes before registration |
| `registerCustomNodes` | Allows extensions to register additional nodes |
| `loadedGraphNode` | Called when a node is reloaded onto the graph |
| `nodeCreated` | Called after a node's constructor |
| `beforeConfigureGraph` | Called before a graph is configured |
| `afterConfigureGraph` | Called after a graph is configured |
| `getSelectionToolboxCommands` | Allows extensions to add commands to the selection toolbox |
For the complete list of available hooks and detailed descriptions, see the [ComfyExtension interface in comfy.ts](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/types/comfy.ts).

View File

@@ -19,6 +19,7 @@
### How Extensions Load
**Backend Flow (Python Custom Nodes):**
1. ComfyUI server starts → scans `/custom_nodes/` directories
2. Loads Python modules (e.g., `/custom_nodes/ComfyUI-Impact-Pack/__init__.py`)
3. Python code registers new node types with the server
@@ -27,12 +28,14 @@
**Frontend Flow (JavaScript):**
*Core Extensions (always available):*
_Core Extensions (always available):_
1. Built directly into the frontend bundle at `/src/extensions/core/`
2. Loaded immediately when the frontend starts
3. No network requests needed - they're part of the compiled code
*Custom Node JavaScript (loaded dynamically):*
_Custom Node JavaScript (loaded dynamically):_
1. Frontend starts → calls `/extensions` API
2. Server responds with list of JavaScript files from:
- `/web/extensions/*.js` (legacy location)
@@ -42,6 +45,7 @@
5. These registered hooks enhance the UI for their associated Python nodes
**The Key Distinction:**
- **Python nodes** = Backend processing (what shows in your node menu)
- **JavaScript extensions** = Frontend enhancements (how nodes look/behave in the UI)
- A custom node package can have both, just Python, or (rarely) just JavaScript
@@ -58,6 +62,7 @@ ComfyUI migrated to TypeScript and Vite, but thousands of extensions rely on the
**Production Build:**
During production build, a custom Vite plugin:
- Binds all module exports to `window.comfyAPI`
- Generates shim files that re-export from this global object
@@ -73,6 +78,7 @@ import { api } from '/scripts/api.js'
```
**Why Dev Server Can't Support This:**
- The dev server serves raw source files without bundling
- Vite refuses to transform node_modules in unbundled mode
- Creating real-time shims would require intercepting every module request
@@ -81,6 +87,7 @@ import { api } from '/scripts/api.js'
### The Trade-off
This was the least friction approach:
- ✅ Extensions work in production without changes
- ✅ Developers get modern tooling (TypeScript, hot reload)
- ❌ Extension testing requires production build or workarounds
@@ -104,11 +111,13 @@ The alternative would have been breaking all existing extensions or staying with
### Option 2: Use Production Build
Build the frontend for full functionality:
```bash
pnpm build
```
For faster iteration during development, use watch mode:
```bash
pnpm exec vite build --watch
```
@@ -118,6 +127,7 @@ Note: Watch mode provides faster rebuilds than full builds, but still no hot rel
### Option 3: Test Against Cloud/Staging
For cloud extensions, modify `.env`:
```
DEV_SERVER_COMFYUI_URL=http://stagingcloud.comfy.org/
```
@@ -133,4 +143,4 @@ DEV_SERVER_COMFYUI_URL=http://stagingcloud.comfy.org/
- [Core Extensions Architecture](./core.md) - Complete list of core extensions and development guidelines
- [JavaScript Extension Hooks](https://docs.comfy.org/custom-nodes/js/javascript_hooks) - Official documentation on extension hooks
- [ComfyExtension Interface](../../src/types/comfy.ts) - TypeScript interface defining all extension capabilities
- [ComfyExtension Interface](../../src/types/comfy.ts) - TypeScript interface defining all extension capabilities

View File

@@ -17,6 +17,7 @@ See `docs/testing/*.md` for detailed patterns.
## Test Tags
Tags are respected by config:
- `@mobile` - Mobile viewport tests
- `@2x` - High DPI tests

View File

@@ -8,6 +8,7 @@ globs:
## File Placement
Place `*.stories.ts` files alongside their components:
```
src/components/MyComponent/
├── MyComponent.vue
@@ -30,13 +31,16 @@ export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: { /* props */ }
args: {
/* props */
}
}
```
## Required Story Variants
Include when applicable:
- **Default** - Minimal props
- **WithData** - Realistic data
- **Loading** - Loading state

View File

@@ -43,7 +43,7 @@ pnpm test:unit
# Run a specific test file
pnpm test:unit -- src/path/to/file.test.ts
# Run unit tests in watch mode
# Run unit tests in watch mode
pnpm test:unit -- --watch
```

View File

@@ -79,7 +79,7 @@ describe('ColorCustomizationSelector', () => {
}
})
}
it('initializes with predefined color when provided', async () => {
const wrapper = mountComponent({
modelValue: '#0d6efd'
@@ -109,16 +109,16 @@ describe('SidebarIcon with tooltip', () => {
it('shows tooltip on hover', async () => {
const tooltipShowDelay = 300
const tooltipText = 'Settings'
const wrapper = mount(SidebarIcon, {
global: {
plugins: [PrimeVue],
directives: { tooltip: Tooltip }
},
props: {
props: {
icon: 'pi pi-cog',
selected: false,
tooltip: tooltipText
tooltip: tooltipText
}
})
@@ -137,13 +137,13 @@ describe('SidebarIcon with tooltip', () => {
plugins: [PrimeVue],
directives: { tooltip: Tooltip }
},
props: {
icon: 'pi pi-cog',
props: {
icon: 'pi pi-cog',
selected: false,
tooltip: tooltipText
tooltip: tooltipText
}
})
expect(wrapper.attributes('aria-label')).toEqual(tooltipText)
})
})
@@ -196,10 +196,10 @@ describe('EditableText', () => {
// Initially in view mode
expect(wrapper.find('input').exists()).toBe(false)
// Click to edit
await wrapper.find('.editable-text').trigger('click')
// Should switch to edit mode
expect(wrapper.find('input').exists()).toBe(true)
expect(wrapper.find('input').element.value).toBe('Initial Text')
@@ -215,17 +215,17 @@ describe('EditableText', () => {
// Switch to edit mode
await wrapper.find('.editable-text').trigger('click')
// Change input value
const input = wrapper.find('input')
await input.setValue('New Text')
// Press enter to save
await input.trigger('keydown.enter')
// Check if event was emitted with new value
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['New Text'])
// Should switch back to view mode
expect(wrapper.find('input').exists()).toBe(false)
})
@@ -247,17 +247,17 @@ it('shows dropdown options when clicked', async () => {
selectedVersion: '1.1.0'
}
})
// Initially dropdown should be hidden
expect(wrapper.find('.p-dropdown-panel').isVisible()).toBe(false)
// Click dropdown
await wrapper.find('.p-dropdown').trigger('click')
await nextTick() // Wait for Vue to update the DOM
// Dropdown should be visible now
expect(wrapper.find('.p-dropdown-panel').isVisible()).toBe(true)
// Options should match the provided versions
const options = wrapper.findAll('.p-dropdown-item')
expect(options.length).toBe(3)
@@ -286,7 +286,7 @@ const waitForPromises = async () => {
it('fetches versions on mount', async () => {
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
mountComponent()
await waitForPromises() // Wait for async operations and reactivity
@@ -302,13 +302,14 @@ When components use `onMounted` or other lifecycle hooks with async operations:
it('shows loading state while fetching versions', async () => {
// Delay the promise resolution
mockGetPackVersions.mockImplementationOnce(
() => new Promise((resolve) =>
setTimeout(() => resolve(defaultMockVersions), 1000)
)
() =>
new Promise((resolve) =>
setTimeout(() => resolve(defaultMockVersions), 1000)
)
)
const wrapper = mountComponent()
// Check loading state before promises resolve
expect(wrapper.text()).toContain('Loading versions...')
})
@@ -347,13 +348,13 @@ Testing components with computed properties that depend on async data:
```typescript
it('displays special options and version options in the listbox', async () => {
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const wrapper = mountComponent()
await waitForPromises() // Wait for data fetching and computed property updates
const listbox = wrapper.findComponent(Listbox)
const options = listbox.props('options')!
// Now options should be populated through computed properties
expect(options.length).toBe(defaultMockVersions.length + 2)
})
@@ -367,4 +368,4 @@ it('displays special options and version options in the listbox', async () => {
4. **PrimeVue components**: PrimeVue components often have their own internal state and reactivity that needs time to update
5. **Computed properties depending on async data**: Always ensure async data is loaded before testing computed properties
By using the `waitForPromises` helper and being mindful of these patterns, you can write more robust tests for components with complex reactivity.
By using the `waitForPromises` helper and being mindful of these patterns, you can write more robust tests for components with complex reactivity.

View File

@@ -29,10 +29,10 @@ describe('useWorkflowStore', () => {
beforeEach(() => {
// Create a fresh pinia and activate it for each test
setActivePinia(createPinia())
// Initialize the store
store = useWorkflowStore()
// Clear any mocks
vi.clearAllMocks()
})
@@ -119,18 +119,21 @@ describe('getters', () => {
beforeEach(() => {
setActivePinia(createPinia())
store = useModelStore()
// Set up test data
store.models = {
checkpoints: [
{ name: 'model1.safetensors', path: 'models/checkpoints/model1.safetensors' },
{
name: 'model1.safetensors',
path: 'models/checkpoints/model1.safetensors'
},
{ name: 'model2.ckpt', path: 'models/checkpoints/model2.ckpt' }
],
loras: [
{ name: 'lora1.safetensors', path: 'models/loras/lora1.safetensors' }
]
}
// Mock API
vi.mocked(api.getModelInfo).mockImplementation(async (modelName) => {
if (modelName.includes('model1')) {
@@ -277,4 +280,4 @@ describe('renameWorkflow', () => {
expect(bookmarkStore.isBookmarked('workflows/dir/test.json')).toBe(false)
})
})
```
```

View File

@@ -12,7 +12,6 @@ This guide covers patterns and examples for unit testing utilities, composables,
6. [Testing with Debounce and Throttle](#testing-with-debounce-and-throttle)
7. [Mocking Node Definitions](#mocking-node-definitions)
## Testing Vue Composables with Reactivity
Testing Vue composables requires handling reactivity correctly:
@@ -37,16 +36,18 @@ describe('useServerLogs', () => {
// Simulate log event handler being called
const mockHandler = vi.mocked(useEventListener).mock.calls[0][2]
mockHandler(new CustomEvent('logs', {
detail: {
type: 'logs',
entries: [{ m: 'Log message' }]
}
}))
mockHandler(
new CustomEvent('logs', {
detail: {
type: 'logs',
entries: [{ m: 'Log message' }]
}
})
)
// Must wait for Vue reactivity to update
await nextTick()
expect(logs.value).toEqual(['Log message'])
})
})
@@ -72,12 +73,12 @@ describe('LGraph', () => {
it('should serialize graph nodes', async () => {
// Register node type
LiteGraph.registerNodeType('dummy', DummyNode)
// Create graph with nodes
const graph = new LGraph()
const node = new DummyNode()
graph.add(node)
// Test serialization
const result = graph.serialize()
expect(result.nodes).toHaveLength(1)
@@ -99,18 +100,18 @@ import { defaultGraph } from '@/scripts/defaultGraph'
describe('workflow validation', () => {
it('should validate default workflow', async () => {
const validWorkflow = JSON.parse(JSON.stringify(defaultGraph))
// Validate workflow
const result = await validateComfyWorkflow(validWorkflow)
expect(result).not.toBeNull()
})
it('should handle position format conversion', async () => {
const workflow = JSON.parse(JSON.stringify(defaultGraph))
// Legacy position format as object
workflow.nodes[0].pos = { '0': 100, '1': 200 }
// Should convert to array format
const result = await validateComfyWorkflow(workflow)
expect(result.nodes[0].pos).toEqual([100, 200])
@@ -139,7 +140,7 @@ vi.mock('@/scripts/api', () => ({
it('should subscribe to logs API', () => {
// Call function that uses the API
startListening()
// Verify API was called correctly
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
})
@@ -167,9 +168,9 @@ describe('Function using debounce', () => {
it('calls debounced function immediately in tests', () => {
const mockFn = vi.fn()
const debouncedFn = debounce(mockFn, 1000)
debouncedFn()
// No need to wait - our mock makes it execute immediately
expect(mockFn).toHaveBeenCalled()
})
@@ -190,25 +191,25 @@ describe('debounced function', () => {
})
afterEach(() => {
vi.useRealTimers()
vi.useRealTimers()
})
it('should debounce function calls', () => {
const mockFn = vi.fn()
const debouncedFn = debounce(mockFn, 1000)
// Call multiple times
debouncedFn()
debouncedFn()
debouncedFn()
// Function not called yet (debounced)
expect(mockFn).not.toHaveBeenCalled()
// Advance time just before debounce period
vi.advanceTimersByTime(999)
expect(mockFn).not.toHaveBeenCalled()
// Advance to debounce completion
vi.advanceTimersByTime(1)
expect(mockFn).toHaveBeenCalledTimes(1)
@@ -223,7 +224,10 @@ Creating mock node definitions for testing:
```typescript
// Example from: tests-ui/tests/apiTypes.test.ts
import { describe, expect, it } from 'vitest'
import { type ComfyNodeDef, validateComfyNodeDef } from '@/schemas/nodeDefSchema'
import {
type ComfyNodeDef,
validateComfyNodeDef
} from '@/schemas/nodeDefSchema'
// Create a complete mock node definition
const EXAMPLE_NODE_DEF: ComfyNodeDef = {
@@ -248,4 +252,4 @@ const EXAMPLE_NODE_DEF: ComfyNodeDef = {
it('should validate node definition', () => {
expect(validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toBeNull()
})
```
```

View File

@@ -36,7 +36,7 @@ describe('MyStore', () => {
```typescript
beforeEach(() => {
vi.resetAllMocks() // Not individual mock.mockReset() calls
vi.resetAllMocks() // Not individual mock.mockReset() calls
})
```
@@ -75,9 +75,9 @@ When a store registers event listeners at module load time:
```typescript
function getEventHandler() {
const call = vi.mocked(api.addEventListener).mock.calls.find(
([event]) => event === 'my_event'
)
const call = vi
.mocked(api.addEventListener)
.mock.calls.find(([event]) => event === 'my_event')
return call?.[1] as (e: CustomEvent<MyEventType>) => void
}