the "hello world" node is working

This commit is contained in:
Claire Vlases
2026-02-22 16:21:39 -05:00
commit 773bafb62a
29 changed files with 11018 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
end_of_line = lf
[*.bat]
indent_style = tab
end_of_line = crlf
[LICENSE]
insert_final_newline = false
[Makefile]
indent_style = tab
+40
View File
@@ -0,0 +1,40 @@
name: React Extension Build + Publish to Comfy Registry
on:
push:
branches: [ "main" ]
paths:
- 'pyproject.toml'
jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'Comfy-Org' }}
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: |
cd ui
npm install
- name: Build React extension
run: |
cd ui
npm run build
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
skip_checkout: 'true'
+114
View File
@@ -0,0 +1,114 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# OSX useful to ignore
*.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# C extensions
*.so
# Distribution / packaging
.Python
env/
venv/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# IntelliJ Idea
.idea
*.iml
*.ipr
*.iws
# PyBuilder
target/
# Cookiecutter
output/
python_boilerplate/
cookiecutter-pypackage-env/
# vscode settings
.history/
*.code-workspace
# Frontend extension
node_modules/
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
node.zip
.vscode/
.claude/
+10
View File
@@ -0,0 +1,10 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.9
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2026, Claire Vlases
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+229
View File
@@ -0,0 +1,229 @@
# creative-code
![react-example-demo](https://github.com/cvlases/creative_code/blob/assets-branch/docs/demo.gif)
![demo pic](https://github.com/cvlases/creative_code/blob/assets-branch/react-example-demo.png)
A minimal template for creating React/TypeScript frontend extensions for ComfyUI, with complete boilerplate setup.
📚 **[ComfyUI JavaScript Developer Documentation](https://docs.comfy.org/custom-nodes/js/javascript_overview)** - Learn how to use ComfyUI's powerful extension APIs.
## Features
- **React & TypeScript Integration**: Ready-to-use setup for creating modern UI components within ComfyUI
- **Internationalization Framework**: Built-in i18n support with English and Chinese examples
- **ComfyUI API Integration**: Properly typed access to ComfyUI's internal API
- **Full TypeScript Support**: Type-safe code using ComfyUI's official type definitions
- **Auto-Reload Development**: Watch mode for seamless development experience
## Installation
### From ComfyUI Registry (Recommended)
The easiest way to install this extension is through the ComfyUI Manager:
1. Open ComfyUI and go to the Manager
2. Search for "React Extension Template"
3. Click Install
### Manual Installation
If you want to install directly from GitHub for development purposes:
```bash
# Go to your ComfyUI custom_nodes directory
cd ComfyUI/custom_nodes
# Clone the repository
git clone https://github.com/cvlases/creative_code.git
# Build the React application
cd creative_code/ui
npm install
npm run build
# Restart ComfyUI
```
⚠️ **Important**: When installing manually from GitHub, you **must** run `npm run build` in the `ui/` directory before the extension will work. The extension requires the compiled React code in the `dist/` folder to function properly in ComfyUI.
## Usage
This template includes a simple example extension that displays workflow node statistics. After installation:
1. Look for the "React Example" tab in the ComfyUI sidebar
2. Click to open the example UI
When developing your own extension, you can:
1. Replace the example UI in App.tsx with your own components
2. Update the tab title and icon in main.tsx
3. Customize the extension's appearance and behavior
## Development
### Setup Development Environment
```bash
# Go to the UI directory
cd ui
# Install dependencies
npm install
# Start development mode (watches for changes)
npm run watch
```
### Available ComfyUI Extension APIs
This template provides access to ComfyUI's powerful JavaScript APIs through the official type definitions. You can use these APIs to build rich extensions:
- **Sidebar Tabs**: Create custom sidebar panels like this template demonstrates
- **Bottom Bar Panels**: Add panels to the bottom of the UI
- **Top Menu Items**: Add custom entries to the top menu
- **Context Menus**: Create custom context menus for the graph
- **Settings**: Add settings to the ComfyUI settings panel
- **Toasts**: Display notification messages
- **Commands**: Create and register custom commands
- **Hotkeys/Keybindings**: Register custom keyboard shortcuts
- **About Panel Badges**: Add badges to the about panel
- **App Events**: Listen to and respond to app events
- **Graph Manipulation**: Programmatically manipulate the workflow graph
For comprehensive documentation on all available APIs, see the [ComfyUI JavaScript Developer Documentation](https://docs.comfy.org/custom-nodes/js/javascript_overview).
### File Structure
```
creative_code/
├── .github/ # GitHub configurations
│ └── workflows/
│ └── react-build.yml # Automatic build and publishing workflow
├── __init__.py # Python entry point for ComfyUI integration
├── pyproject.toml # Project metadata for ComfyUI Registry
├── dist/ # Built extension files (generated)
└── ui/ # React application
├── public/
│ └── locales/ # Internationalization files
│ ├── en/
│ │ └── main.json # English translations
│ └── zh/
│ └── main.json # Chinese translations
├── src/
│ ├── App.tsx # Main React component with example UI
│ ├── App.css # Styles for the example UI
│ ├── index.css # Global styles and theme variables
│ ├── main.tsx # Entry point for React app
│ ├── vite-env.d.ts # Vite environment types
│ ├── setupTests.ts # Testing environment setup
│ ├── __tests__/ # Unit tests for components
│ │ └── dummy.test.tsx # Example test
│ └── utils/
│ └── i18n.ts # Internationalization setup
├── eslint.config.js # ESLint configuration
├── jest.config.js # Jest testing configuration
├── jest.setup.js # Jest setup file
├── package.json # npm dependencies
├── tsconfig.json # TypeScript configuration
├── tsconfig.node.json # TypeScript configuration for Node
└── vite.config.ts # Build configuration
```
### TypeScript Support
This extension uses the official `@comfyorg/comfyui-frontend-types` package for type-safe interaction with ComfyUI APIs. To install it:
```bash
cd ui
npm install -D @comfyorg/comfyui-frontend-types
```
## Publishing to ComfyUI Registry
### Prerequisites
1. Set up a [Registry](https://registry.comfy.org) account
2. Create an API key at https://registry.comfy.org/nodes
### Steps to Publish
1. Install the comfy-cli tool:
```bash
pip install comfy-cli
```
2. Verify your pyproject.toml has the correct metadata:
```toml
[project]
name = "your_extension_name" # Use a unique name for your extension
description = "Your extension description here."
version = "0.1.0" # Increment this with each update
[tool.comfy]
PublisherId = "your_publisher_id" # Your Registry publisher ID
DisplayName = "Your Extension Display Name"
includes = ["dist/"] # Include built React code (normally ignored by .gitignore)
```
3. Publish your extension:
```bash
comfy-cli publish
```
4. When prompted, enter your API key
### Automatic Publishing with GitHub Actions
This template includes a GitHub Actions workflow that automatically publishes to the ComfyUI Registry whenever you update the version in pyproject.toml:
1. Go to your repository's Settings > Secrets and variables > Actions
2. Create a new repository secret called `REGISTRY_ACCESS_TOKEN` with your API key
3. Commit and push an update to pyproject.toml (e.g., increment the version number)
4. The GitHub Action will automatically run and publish your extension
The workflow configuration is set up in `.github/workflows/react-build.yml` and will trigger when:
- The `pyproject.toml` file is modified and pushed to the `main` branch
The workflow automatically:
1. Sets up Node.js environment
2. Installs dependencies (`npm install`)
3. Builds the React extension (`npm run build`)
4. Publishes the extension to the ComfyUI Registry
## Unit Testing
This template includes a basic setup for unit testing with Jest and React Testing Library:
```bash
# Run tests
npm test
# Run tests in watch mode during development
npm run test:watch
```
Example tests can be found in the `src/__tests__` directory. The setup includes:
- Jest for running tests
- React Testing Library for testing components
- Mock implementation of the ComfyUI window.app object
## Resources
- [ComfyUI JS Extension Documentation](https://docs.comfy.org/custom-nodes/js/javascript_overview) - Official documentation for ComfyUI JavaScript Extensions
- [ComfyUI Registry Documentation](https://docs.comfy.org/registry/publishing) - Learn how to publish your extension
- [ComfyUI Frontend Repository](https://github.com/cvlases/ComfyUI-Frontend) - The main ComfyUI frontend codebase
- [Official ComfyUI Frontend Types](https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types) - TypeScript definitions for ComfyUI
- [React Extension Guide](REACT_EXTENSION_GUIDE.md) - Detailed guide for creating React extensions
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
- [React Documentation](https://react.dev/reference/react)
- [Jest Documentation](https://jestjs.io/docs/getting-started)
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
## Contributing
Contributions are welcome! Feel free to open issues or submit pull requests to improve this template.
## License
MIT license
+72
View File
@@ -0,0 +1,72 @@
import os
import server
from aiohttp import web
import folder_paths
import nodes
class MyNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {}}
RETURN_TYPES = ()
RETURN_NAMES = ()
FUNCTION = "test"
CATEGORY = "creative-code"
def test(self):
return ()
NODE_CLASS_MAPPINGS = {
"MyNode": MyNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MyNode": "My First Node"
}
__all__ = ["NODE_CLASS_MAPPINGS"]
# Define the path to our extension
workspace_path = os.path.dirname(__file__)
dist_path = os.path.join(workspace_path, "dist/example_ext")
dist_locales_path = os.path.join(workspace_path, "dist/locales")
# Print the current paths for debugging
print(f"ComfyUI_example_frontend_extension workspace path: {workspace_path}")
print(f"Dist path: {dist_path}")
print(f"Dist locales path: {dist_locales_path}")
print(f"Locales exist: {os.path.exists(dist_locales_path)}")
# Register the static route for serving our React app assets
if os.path.exists(dist_path):
# Add the routes for the extension
server.PromptServer.instance.app.add_routes([
web.static("/example_ext/", dist_path),
])
# Register the locale files route
if os.path.exists(dist_locales_path):
server.PromptServer.instance.app.add_routes([
web.static("/locales/", dist_locales_path),
])
print(f"Registered locale files route at /locales/")
else:
print("WARNING: Locale directory not found!")
# Also register the standard ComfyUI extension web directory
project_name = os.path.basename(workspace_path)
try:
# Method added in https://github.com/comfyanonymous/ComfyUI/pull/8357
from comfy_config import config_parser
project_config = config_parser.extract_node_configuration(workspace_path)
project_name = project_config.project.name
print(f"project name read from pyproject.toml: {project_name}")
except Exception as e:
print(f"Could not load project config, using default name '{project_name}': {e}")
nodes.EXTENSION_WEB_DIRS[project_name] = os.path.join(workspace_path, "dist")
else:
print("ComfyUI Example React Extension: Web directory not found")
+86
View File
@@ -0,0 +1,86 @@
[build-system]
requires = ["setuptools>=70.0"]
build-backend = "setuptools.build_meta"
[project]
name = "creative_code"
version = "1.0.0"
description = "explainable AI for comfy"
authors = [
{name = "Claire Vlases", email = "clairevlases@gmail.com"}
]
readme = "README.md"
license = {text = "MIT license"}
requires-python = ">=3.10"
classifiers = []
dependencies = [
]
[project.optional-dependencies]
dev = [
"bump-my-version",
"coverage", # testing
"mypy", # linting
"pre-commit", # runs linting on commit
"pytest", # testing
"ruff", # linting
]
[project.urls]
Repository = "https://github.com/cvlases/creative_code"
BugTracker = "https://github.com/cvlases/creative_code/issues"
Documentation = "https://github.com/cvlases/creative_code/wiki"
[tool.comfy]
PublisherId = "cvlases"
DisplayName = "creative-code"
Icon = ""
Tags = []
Repository = "https://github.com/cvlases/creative_code"
includes = ["dist/"]
[tool.setuptools.package-data]
"*" = ["*.*"]
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = [
"tests",
]
[tool.mypy]
files = "."
# Use strict defaults
strict = true
warn_unreachable = true
warn_no_return = true
[[tool.mypy.overrides]]
# Don't require test functions to include types
module = "tests.*"
allow_untyped_defs = true
disable_error_code = "attr-defined"
[tool.ruff]
# extend-exclude = ["static", "ci/templates"]
line-length = 140
src = ["src", "tests"]
target-version = "py39"
# Add rules to ban exec/eval
[tool.ruff.lint]
select = [
"S102", # exec-builtin
"S307", # eval-used
"W293",
"F", # The "F" series in Ruff stands for "Pyflakes" rules, which catch various Python syntax errors and undefined names.
# See all rules here: https://docs.astral.sh/ruff/rules/#pyflakes-f
]
[tool.ruff.lint.flake8-quotes]
inline-quotes = "double"
+35
View File
@@ -0,0 +1,35 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:react/jsx-runtime",
"plugin:prettier/recommended"
],
"ignorePatterns": ["dist", ".eslintrc.json"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react-refresh", "unused-imports"],
"rules": {
"react-refresh/only-export-components": [
"warn",
{ "allowConstantExport": true }
],
"unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-explicit-any": "off"
},
"settings": {
"react": {
"version": "detect"
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"trailingComma": "none",
"printWidth": 80,
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"overrides": [
{
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
"options": {
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
}
]
}
+39
View File
@@ -0,0 +1,39 @@
import pluginJs from '@eslint/js'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import unusedImports from 'eslint-plugin-unused-imports'
import globals from 'globals'
import tseslint from 'typescript-eslint'
export default [
{
files: ['src/**/*.{js,mjs,cjs,ts,tsx}']
},
{
languageOptions: {
globals: {
...globals.browser
},
parser: tseslint.parser,
parserOptions: {
project: './tsconfig.json',
ecmaVersion: 2020,
sourceType: 'module'
}
}
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
eslintPluginPrettierRecommended,
{
plugins: {
'unused-imports': unusedImports
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off',
'unused-imports/no-unused-imports': 'error'
}
}
]
+16
View File
@@ -0,0 +1,16 @@
export default {
testEnvironment: 'jsdom',
transform: {
'^.+\\.(ts|tsx)$': ['ts-jest', {
useESM: true,
}],
},
moduleNameMapper: {
// Handle CSS imports (with CSS modules)
'\\.css$': 'identity-obj-proxy',
},
extensionsToTreatAsEsm: ['.ts', '.tsx'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)']
};
+16
View File
@@ -0,0 +1,16 @@
// Import jest-dom additions
import '@testing-library/jest-dom';
// Mock window.app for ComfyUI integration testing
global.window.app = {
graph: {
_nodes: []
},
api: {
addEventListener: jest.fn(),
removeEventListener: jest.fn()
},
canvas: {
centerOnNode: jest.fn()
}
};
+8825
View File
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
{
"name": "creative_code",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"watch": "tsc && vite build --watch",
"lint": "eslint . --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --ext ts,tsx,js,jsx --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
"typecheck": "tsc --noEmit",
"preview": "vite preview",
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"i18next": "^23.10.2",
"i18next-browser-languagedetector": "^7.2.2",
"i18next-http-backend": "^2.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0"
},
"devDependencies": {
"@comfyorg/comfyui-frontend-types": "^1.20.2",
"@eslint/eslintrc": "^3.0.2",
"@eslint/js": "^9.27.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/jest": "^29.5.14",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-unused-imports": "^4.1.4",
"globals": "^16.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^3.5.3",
"ts-jest": "^29.3.4",
"typescript": "^5.4.2",
"typescript-eslint": "^8.32.1",
"vite": "^5.2.10"
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"app": {
"title": "React Example Extension",
"description": "Shows statistics about nodes in the current workflow",
"noNodes": "No nodes in the workflow",
"nodeList": {
"title": "Node List",
"id": "ID",
"type": "Type",
"category": "Category",
"inputs": "Inputs",
"outputs": "Outputs"
},
"nodeStats": {
"title": "Node Statistics",
"totalNodes": "Total nodes",
"uniqueNodeTypes": "Unique node types"
},
"footer": {
"clickToHighlight": "Click on any node in the list to highlight it in the workflow"
}
},
"Click on any node in the list to highlight it in the workflow": "Click on any node in the list to highlight it in the workflow"
}
+24
View File
@@ -0,0 +1,24 @@
{
"app": {
"title": "React 示例扩展",
"description": "显示当前工作流中节点的统计信息",
"noNodes": "工作流中没有节点",
"nodeList": {
"title": "节点列表",
"id": "ID",
"type": "类型",
"category": "类别",
"inputs": "输入",
"outputs": "输出"
},
"nodeStats": {
"title": "节点统计",
"totalNodes": "节点总数",
"uniqueNodeTypes": "唯一节点类型"
},
"footer": {
"clickToHighlight": "单击列表中的任何节点以在工作流中突出显示它"
}
},
"Click on any node in the list to highlight it in the workflow": "单击列表中的任何节点以在工作流中突出显示它"
}
+234
View File
@@ -0,0 +1,234 @@
.react-example-container {
padding: 16px;
height: 100%;
max-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--example-bg-color);
color: var(--example-text-color);
overflow-y: auto;
}
.react-example-container h2 {
margin-top: 0;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--example-border-color);
color: var(--example-primary-color);
}
.react-example-container h3 {
margin-top: 0;
margin-bottom: 12px;
color: var(--example-text-color);
font-size: 1.1em;
}
/* Stats Overview */
.stats-overview {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
padding: 12px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: var(--example-primary-color);
}
.stat-label {
font-size: 12px;
color: var(--example-muted-text-color);
margin-top: 4px;
}
/* Dashboard Sections */
.dashboard-section {
margin-bottom: 20px;
padding: 16px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
/* Node Chart */
.node-stats-chart {
display: flex;
height: 120px;
align-items: flex-end;
gap: 12px;
margin-top: 16px;
padding-bottom: 24px;
position: relative;
}
.chart-bar-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.chart-bar {
width: 24px;
min-height: 4px;
border-radius: 4px 4px 0 0;
transition: height 0.3s ease;
}
.chart-label {
position: absolute;
bottom: -24px;
font-size: 10px;
color: var(--example-muted-text-color);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
transform: rotate(-45deg);
transform-origin: top left;
margin-left: 12px;
}
/* Category Filter */
.category-filter {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.filter-button {
background-color: transparent;
border: none;
color: var(--example-text-color);
padding: 4px 8px;
font-size: 12px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
border-bottom: 3px solid transparent;
}
.filter-button:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.filter-button.active {
background-color: rgba(255, 255, 255, 0.1);
font-weight: bold;
}
/* Node List */
.node-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 150px;
overflow-y: auto;
padding-right: 8px;
}
.node-item {
display: flex;
align-items: center;
padding: 8px 12px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
cursor: pointer;
transition:
transform 0.2s,
background-color 0.2s;
}
.node-item:hover {
background-color: rgba(0, 0, 0, 0.2);
transform: translateX(2px);
}
.node-badge {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 12px;
}
.node-title {
flex: 1;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-meta {
display: flex;
gap: 8px;
font-size: 11px;
color: var(--example-muted-text-color);
}
/* Empty State */
.empty-state {
padding: 20px;
text-align: center;
color: var(--example-muted-text-color);
font-style: italic;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
/* Footer */
.footer {
margin-top: auto;
padding-top: 16px;
border-top: 1px solid var(--example-border-color);
font-size: 0.8em;
text-align: center;
color: var(--example-muted-text-color);
}
/* API Examples Section */
.api-examples {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.api-examples h4 {
width: 100%;
margin-top: 10px;
margin-bottom: 5px;
color: var(--example-text-color);
}
/* Scrollbar styling */
.node-list::-webkit-scrollbar {
width: 6px;
}
.node-list::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
}
.node-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.node-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
+543
View File
@@ -0,0 +1,543 @@
import { ComfyApp } from '@comfyorg/comfyui-frontend-types'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import './App.css'
// Type definitions for the global ComfyUI objects
declare global {
interface Window {
app?: ComfyApp
}
}
// Interface for our processed node data
interface ProcessedNode {
id: string | number
title: string
type: string
category: string
inputs: number
outputs: number
pos: [number, number]
}
// Type for category colors
type CategoryColors = Record<string, string>
const CATEGORY_COLORS: CategoryColors = {
loaders: '#7e57c2',
conditioning: '#26a69a',
sampling: '#ef5350',
latent: '#66bb6a',
image: '#42a5f5',
mask: '#ff9800',
'conditioning/clip': '#26a69a',
'image/postprocessing': '#ec407a',
advanced: '#5c6bc0',
_default: '#78909c'
}
interface NodeStatsChartProps {
nodeCounts: Record<string, number>
totalNodes: number
}
function NodeStatsChart({ nodeCounts, totalNodes }: NodeStatsChartProps) {
// Only show the top 8 categories for the chart
const topCategories = Object.entries(nodeCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
const chartData = topCategories.map(([category, count]) => {
return {
category,
count,
percentage: Math.round((count / totalNodes) * 100)
}
})
return (
<div className="node-stats-chart">
{chartData.map((item) => (
<div key={item.category} className="chart-bar-container">
<div
className="chart-bar"
style={{
height: `${item.percentage * 2}px`,
backgroundColor:
CATEGORY_COLORS[item.category] || CATEGORY_COLORS._default
}}
/>
<div className="chart-label">{item.category}</div>
</div>
))}
</div>
)
}
interface CategoryFilterProps {
categories: string[]
selectedCategory: string
onSelectCategory: (category: string) => void
}
function CategoryFilter({
categories,
selectedCategory,
onSelectCategory
}: CategoryFilterProps) {
// Using useTranslation hook to initialize i18n context
useTranslation()
return (
<div className="category-filter">
<button
className={
selectedCategory === 'all' ? 'filter-button active' : 'filter-button'
}
onClick={() => onSelectCategory('all')}
>
All
</button>
{categories.map((category) => (
<button
key={category}
className={
selectedCategory === category
? 'filter-button active'
: 'filter-button'
}
onClick={() => onSelectCategory(category)}
style={{
borderBottom: `3px solid ${
CATEGORY_COLORS[category] || CATEGORY_COLORS._default
}`
}}
>
{category}
</button>
))}
</div>
)
}
function App() {
const { t } = useTranslation()
const [nodes, setNodes] = useState<ProcessedNode[]>([])
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [isQueueRunning, setIsQueueRunning] = useState<boolean>(false)
const [highlightedNode, setHighlightedNode] = useState<
string | number | null
>(null)
// Get nodes from ComfyUI graph and organize them
useEffect(() => {
if (!window.app?.graph) return
// Initial node collection
collectNodes()
// Function to collect nodes from the graph
function collectNodes() {
const graphNodes = window.app?.graph._nodes
if (!graphNodes) return
const currentNodes: ProcessedNode[] = []
for (const node of graphNodes) {
// Extract category from constructorData if available
let category = ''
if (node.constructor && 'nodeData' in node.constructor) {
const nodeData = node.constructor.nodeData
if (
nodeData &&
typeof nodeData === 'object' &&
'category' in nodeData
) {
category = nodeData.category as string
}
}
currentNodes.push({
id: node.id,
title: node.title,
type: node.type,
category: category,
inputs: node.inputs?.length || 0,
outputs: node.outputs?.length || 0,
pos: [...node.pos] as [number, number] // Clone position array
})
}
setNodes(currentNodes)
}
// Listen for graph changes using the API (not graph directly)
const handleGraphChanged = () => {
collectNodes()
}
window.app?.api.addEventListener('graphChanged', handleGraphChanged)
return () => {
window.app?.api.removeEventListener('graphChanged', handleGraphChanged)
}
}, [])
// Monitor queue status
useEffect(() => {
if (!window.app?.api) return
const handleQueueStart = () => setIsQueueRunning(true)
const handleQueueComplete = () => setIsQueueRunning(false)
// Using the Event API with properly typed events
window.app.api.addEventListener('execution_start', handleQueueStart)
// Since 'execution_complete' is not directly in the types, we add a compatibility approach
type ApiEventName = Parameters<typeof window.app.api.addEventListener>[0]
window.app.api.addEventListener(
'execution_complete' as ApiEventName,
handleQueueComplete
)
return () => {
window.app?.api.removeEventListener('execution_start', handleQueueStart)
window.app?.api.removeEventListener(
'execution_complete' as ApiEventName,
handleQueueComplete
)
}
}, [])
// Handle node highlighting
useEffect(() => {
if (!highlightedNode || !window.app?.graph) return
// Find the node in the graph
const graphNodes = window.app.graph._nodes
const node = graphNodes.find((n) => n.id === highlightedNode)
if (!node) return
// Center camera on node with smooth animation
window.app.canvas.centerOnNode(node)
// Highlight the node
const originalColor = node.color
node.color = '#ff5722'
window.app.graph.setDirtyCanvas(true, false)
// Reset highlight after a delay
const timeout = setTimeout(() => {
if (window.app?.graph._nodes.includes(node)) {
node.color = originalColor
window.app.graph.setDirtyCanvas(true, false)
}
}, 2000)
return () => {
clearTimeout(timeout)
// Restore original color if component unmounts
if (window.app?.graph._nodes.includes(node)) {
node.color = originalColor
window.app.graph.setDirtyCanvas(true, false)
}
}
}, [highlightedNode])
// Calculate statistics from nodes
const { filteredNodes, categories, nodeCounts, totalNodes } = useMemo(() => {
// Get filtered nodes based on selected category
const filtered =
selectedCategory === 'all'
? nodes
: nodes.filter((node) => node.category === selectedCategory)
// Get unique categories
const uniqueCategories = [
...new Set(nodes.map((node) => node.category))
].sort()
// Count nodes by category
const counts = nodes.reduce((acc: Record<string, number>, node) => {
const category = node.category
acc[category] = (acc[category] || 0) + 1
return acc
}, {})
return {
filteredNodes: filtered,
categories: uniqueCategories,
nodeCounts: counts,
totalNodes: nodes.length
}
}, [nodes, selectedCategory])
return (
<div className="react-example-container">
<h2>{t('app.title')}</h2>
<div className="stats-overview">
<div className="stat-card">
<div className="stat-value">{totalNodes}</div>
<div className="stat-label">{t('app.nodeStats.totalNodes')}</div>
</div>
<div className="stat-card">
<div className="stat-value">{categories.length}</div>
<div className="stat-label">{t('app.nodeStats.uniqueNodeTypes')}</div>
</div>
<div className="stat-card">
<div className="stat-value">{isQueueRunning ? 'Active' : 'Idle'}</div>
<div className="stat-label">Queue Status</div>
</div>
</div>
<div className="dashboard-section">
<h3>Node Type Distribution</h3>
{totalNodes > 0 ? (
<NodeStatsChart nodeCounts={nodeCounts} totalNodes={totalNodes} />
) : (
<div className="empty-state">{t('app.noNodes')}</div>
)}
</div>
<div className="dashboard-section">
<h3>{t('app.nodeList.title')}</h3>
<CategoryFilter
categories={categories}
selectedCategory={selectedCategory}
onSelectCategory={setSelectedCategory}
/>
<div className="node-list">
{filteredNodes.length > 0 ? (
filteredNodes.map((node) => (
<div
key={node.id}
className="node-item"
onClick={() => setHighlightedNode(node.id)}
>
<div
className="node-badge"
style={{
backgroundColor:
CATEGORY_COLORS[node.category] || CATEGORY_COLORS._default
}}
></div>
<div className="node-title">{node.title}</div>
<div className="node-meta">
<span>
{t('app.nodeList.inputs')}: {node.inputs}
</span>
<span>
{t('app.nodeList.outputs')}: {node.outputs}
</span>
</div>
</div>
))
) : (
<div className="empty-state">
{totalNodes === 0
? t('app.noNodes')
: 'No nodes match the selected filter'}
</div>
)}
</div>
</div>
<div className="dashboard-section" style={{ marginTop: '20px' }}>
<h3>API Examples</h3>
<div
className="api-examples"
style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}
>
<h4>Dialog API</h4>
<button
className="dialog-btn"
onClick={() => {
// Dialog API Example - Prompt
void window.app?.extensionManager.dialog
.prompt({
title: 'Dialog API Demo',
message:
'This is a prompt dialog example. Please enter something:',
defaultValue: 'Dialog API is great!'
})
.then((result) => {
if (result !== null) {
alert(`You entered: ${result}`)
}
})
}}
style={{
padding: '8px 12px',
backgroundColor: '#2196F3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
margin: '5px'
}}
>
Show Prompt Dialog
</button>
<button
className="dialog-btn"
onClick={() => {
// Dialog API Example - Confirm
void window.app?.extensionManager.dialog
.confirm({
title: 'Confirm Action',
message: 'This is a confirmation dialog example.',
type: 'default'
})
.then((result) => {
alert(result ? 'You confirmed!' : 'You cancelled!')
})
}}
style={{
padding: '8px 12px',
backgroundColor: '#FF9800',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
margin: '5px'
}}
>
Show Confirm Dialog
</button>
<h4>Toast API</h4>
<button
className="toast-btn"
onClick={() => {
// Toast API Example - Info
window.app?.extensionManager.toast.add({
severity: 'info',
summary: 'Information',
detail: 'This is an info toast message',
life: 3000
})
}}
style={{
padding: '8px 12px',
backgroundColor: '#2196F3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
margin: '5px'
}}
>
Show Info Toast
</button>
<button
className="toast-btn"
onClick={() => {
// Toast API Example - Success
window.app?.extensionManager.toast.add({
severity: 'success',
summary: 'Success',
detail: 'Operation completed successfully!',
life: 3000
})
}}
style={{
padding: '8px 12px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
margin: '5px'
}}
>
Show Success Toast
</button>
<button
className="toast-btn"
onClick={() => {
// Toast API Example - Warning
window.app?.extensionManager.toast.add({
severity: 'warn',
summary: 'Warning',
detail: 'This action may cause issues!',
life: 5000
})
}}
style={{
padding: '8px 12px',
backgroundColor: '#FF9800',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
margin: '5px'
}}
>
Show Warning Toast
</button>
<button
className="toast-btn"
onClick={() => {
// Toast API Example - Error
window.app?.extensionManager.toast.add({
severity: 'error',
summary: 'Error',
detail: 'Something went wrong!',
life: 5000
})
}}
style={{
padding: '8px 12px',
backgroundColor: '#F44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
margin: '5px'
}}
>
Show Error Toast
</button>
<button
className="toast-btn"
onClick={() => {
// Toast API Example - Alert alternative using regular toast
window.app?.extensionManager.toast.add({
severity: 'info',
summary: 'Alert',
detail: 'This is an alert message!',
life: 3000
})
}}
style={{
padding: '8px 12px',
backgroundColor: '#673AB7',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
margin: '5px'
}}
>
Show Alert Toast
</button>
</div>
</div>
<div className="footer">
<p>{t('app.footer.clickToHighlight')}</p>
</div>
</div>
)
}
export default App
+18
View File
@@ -0,0 +1,18 @@
/** @jsxImportSource react */
import '@testing-library/jest-dom'
import { render } from '@testing-library/react'
// A simple component for testing
function DummyComponent({ text }: { text: string }) {
return <div data-testid="dummy-component">{text}</div>
}
describe('Dummy Component', () => {
it('renders with provided text', () => {
const { getByTestId } = render(<DummyComponent text="Hello ComfyUI!" />)
const element = getByTestId('dummy-component')
expect(element).toBeInTheDocument()
expect(element).toHaveTextContent('Hello ComfyUI!')
})
})
+33
View File
@@ -0,0 +1,33 @@
/* Base styles for our extension */
:root {
--example-primary-color: #3498db;
--example-secondary-color: #2c3e50;
--example-text-color: #ecf0f1;
--example-accent-color: #e74c3c;
--example-bg-color: #34495e;
--example-border-color: #7f8c8d;
--example-muted-text-color: #95a5a6;
}
/* Light mode overrides */
:root.comfy-theme-light #comfyui-react-example-root {
--example-primary-color: #2980b9;
--example-secondary-color: #34495e;
--example-text-color: #2c3e50;
--example-accent-color: #c0392b;
--example-bg-color: #ecf0f1;
--example-border-color: #bdc3c7;
--example-muted-text-color: #7f8c8d;
}
/* Make sure our styles don't leak out to the rest of ComfyUI */
#comfyui-react-example-root {
height: 100%;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
box-sizing: border-box;
color: var(--example-text-color);
}
#comfyui-react-example-root * {
box-sizing: border-box;
}
+243
View File
@@ -0,0 +1,243 @@
import { ComfyApp } from '@comfyorg/comfyui-frontend-types'
import React, { Suspense } from 'react'
import ReactDOM from 'react-dom/client'
import { useTranslation } from 'react-i18next'
import './index.css'
import './utils/i18n'
// Declare global ComfyUI objects
declare global {
interface Window {
app?: ComfyApp
}
}
// Lazy load the App component for better performance
const App = React.lazy(() => import('./App'))
// Function to wait for document and app to be ready
function waitForInit(): Promise<void> {
return new Promise((resolve) => {
// Check if document is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkApp)
} else {
checkApp()
}
// Check if app is available
function checkApp() {
if (window.app) {
resolve()
} else {
// Poll for app availability
const interval = setInterval(() => {
if (window.app) {
console.log('App initialized')
clearInterval(interval)
resolve()
}
}, 50)
// Set timeout to avoid infinite polling
setTimeout(() => {
clearInterval(interval)
console.error('Timeout waiting for app to initialize')
resolve() // Continue anyway to avoid blocking
}, 5000)
}
}
})
}
// Initialize the extension once everything is ready
async function initializeExtension(): Promise<void> {
try {
// Wait for document and ComfyUI app
await waitForInit()
console.log('App:', window.app)
if (!window.app) {
console.error('ComfyUI app not available')
return
}
// Create a function component with i18n for translation
function SidebarWrapper() {
// Using useTranslation hook to initialize i18n context
useTranslation()
return <App />
}
// Register the sidebar tab using ComfyUI's extension API
const sidebarTab = {
id: 'comfyui-react-example',
icon: 'pi pi-code', // Using PrimeVue icon
title: 'React Example',
tooltip: 'React Example Extension',
type: 'custom' as const,
render: (element: HTMLElement) => {
console.log('Rendering React Example Extension')
// Create a container for our React app
const container = document.createElement('div')
container.id = 'comfyui-react-example-root'
container.style.height = '100%'
element.appendChild(container)
// Mount the React app to the container
ReactDOM.createRoot(container).render(
<React.StrictMode>
<Suspense fallback={<div>Loading...</div>}>
<SidebarWrapper />
</Suspense>
</React.StrictMode>
)
}
}
window.app.extensionManager.registerSidebarTab(sidebarTab)
// Register extension with about page badges
window.app.registerExtension({
name: 'ReactExtensionExample',
// About Panel Badges API - Adds custom badges to the ComfyUI about page
aboutPageBadges: [
{
label: 'Documentation',
url: 'https://docs.comfy.org/custom-nodes/js/javascript_overview',
icon: 'pi pi-file'
},
{
label: 'GitHub',
url: 'https://github.com/Comfy-Org/ComfyUI-React-Extension-Template',
icon: 'pi pi-github'
},
{
label: 'Support',
url: 'https://discord.gg/comfy-org',
icon: 'pi pi-discord'
}
],
// Bottom Panel Tabs API - Adds custom tabs to the bottom panel
bottomPanelTabs: [
{
id: 'react-example-tab',
title: 'React Example Tab',
type: 'custom',
render: (el) => {
// Create a container for our React content
const container = document.createElement('div')
container.id = 'react-example-bottom-tab'
container.style.padding = '10px'
el.appendChild(container)
// Create a React component for the tab content
function TabContent() {
const [count, setCount] = React.useState(0)
return (
<div style={{ padding: '10px' }}>
<h3>React Example Bottom Panel</h3>
<p>This is a demo of the Bottom Panel Tabs API.</p>
<p>Count: {count}</p>
<button
onClick={() => setCount(count + 1)}
style={{
padding: '8px 12px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Increment
</button>
</div>
)
}
// Mount the React component
ReactDOM.createRoot(container).render(
<React.StrictMode>
<TabContent />
</React.StrictMode>
)
}
}
],
// Commands and Keybindings API - Register custom commands with keyboard shortcuts
commands: [
{
id: 'reactExample.showInfo',
label: 'Show React Example Info',
function: () => {
alert(
'React Example Extension - This command was triggered by the Commands API'
)
}
},
{
id: 'reactExample.runWorkflow',
label: 'Run Workflow from React Example',
function: () => {
void window.app?.queuePrompt(0) // Pass 0 as default prompt number
}
},
{
id: 'reactExample.clearWorkflow',
label: 'Clear Workflow from React Example',
function: () => {
if (confirm('Clear the current workflow? This cannot be undone.')) {
window.app?.graph.clear()
}
}
}
],
// Associate keybindings with the commands
keybindings: [
{
combo: { key: 'i', ctrl: true, alt: true },
commandId: 'reactExample.showInfo'
},
{
combo: { key: 'r', ctrl: true, alt: true },
commandId: 'reactExample.runWorkflow'
},
{
combo: { key: 'Delete', ctrl: true, alt: true },
commandId: 'reactExample.clearWorkflow'
}
],
// Topbar Menu API - Add commands to the top menu bar
menuCommands: [
{
// Add commands to the Extensions menu
path: ['Extensions', 'React Example'],
commands: [
'reactExample.showInfo',
'reactExample.runWorkflow',
'reactExample.clearWorkflow'
]
},
{
// Create a submenu under Extensions > React Example
path: ['Extensions', 'React Example', 'Advanced'],
commands: ['reactExample.showInfo']
}
]
})
console.log('React Example Extension initialized successfully')
} catch (error) {
console.error('Failed to initialize React Example Extension:', error)
}
}
// Start initialization
void initializeExtension()
+2
View File
@@ -0,0 +1,2 @@
// Import jest-dom matchers
import '@testing-library/jest-dom'
+135
View File
@@ -0,0 +1,135 @@
import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import Backend from 'i18next-http-backend'
import { initReactI18next } from 'react-i18next'
// Define fallback translations for debugging
interface TranslationResource {
[language: string]: {
[namespace: string]: {
app: {
title: string
description: string
noNodes: string
nodeList: {
title: string
id: string
type: string
category: string
inputs: string
outputs: string
}
nodeStats: {
title: string
totalNodes: string
uniqueNodeTypes: string
}
footer: {
clickToHighlight: string
}
}
}
}
}
const fallbackResources: TranslationResource = {
en: {
main: {
app: {
title: 'React Example Extension (Fallback)',
description: 'Shows statistics about nodes in the current workflow',
noNodes: 'No nodes in the workflow',
nodeList: {
title: 'Node List',
id: 'ID',
type: 'Type',
category: 'Category',
inputs: 'Inputs',
outputs: 'Outputs'
},
nodeStats: {
title: 'Node Statistics',
totalNodes: 'Total nodes',
uniqueNodeTypes: 'Unique node types'
},
footer: {
clickToHighlight:
'Click on any node in the list to highlight it in the workflow'
}
}
}
}
}
// Initialize i18next
void i18n
// Load translations via http backend (must be first!)
.use(Backend)
// Detect user language
.use(LanguageDetector)
// Initialize react-i18next
.use(initReactI18next)
// Initialize i18next
.init({
// Always enable debug mode to see what's happening
debug: true,
// Fallback language
fallbackLng: 'en',
// Namespace for translations
ns: ['main'],
defaultNS: 'main',
// Do not load from bundled resources first
initImmediate: true,
// Custom handling for missing keys
saveMissing: true,
missingKeyHandler: (lng, ns, key) => {
console.log(`Missing translation: [${lng}] ${ns}:${key}`)
},
// Language detection configuration
detection: {
// Order of detection methods
order: ['navigator', 'htmlTag', 'path', 'subdomain'],
// Don't cache detected language
caches: []
},
// Backend configuration
backend: {
// Path to load translations from
loadPath: '/locales/{{lng}}/{{ns}}.json',
// Add retry logic
requestOptions: {
retry: 3,
timeout: 3000
}
},
// React specific options
react: {
useSuspense: true
},
// Allow string formatting for dynamic values
interpolation: {
escapeValue: false // Not needed for React as it escapes by default
}
})
// Add fallback resources only if HTTP loading fails
i18n.on('failedLoading', (lng, ns) => {
console.log(
`Failed loading translation file for ${lng} and ${ns}, using fallback`
)
// Add the fallback resources for this language
if (fallbackResources[lng] && fallbackResources[lng][ns]) {
i18n.addResourceBundle(lng, ns, fallbackResources[lng][ns], true, true)
}
})
export default i18n
+9
View File
@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMeta {
env: {
DEV: boolean
PROD: boolean
MODE: string
}
}
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Testing */
"types": ["jest", "@testing-library/jest-dom"]
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+63
View File
@@ -0,0 +1,63 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
// Plugin to correctly handle the ComfyUI scripts in development mode
const rewriteComfyImports = ({ isDev }) => {
return {
name: "rewrite-comfy-imports",
resolveId(source) {
if (!isDev) {
return;
}
if (source === "/scripts/app.js") {
return "http://127.0.0.1:8188/scripts/app.js";
}
if (source === "/scripts/api.js") {
return "http://127.0.0.1:8188/scripts/api.js";
}
return null;
},
};
};
// Plugin to copy locales to the output directory
const copyLocales = () => {
return {
name: "copy-locales",
writeBundle() {
// This runs after bundle is written
console.log("Bundle complete, copying locales...");
}
};
};
export default defineConfig(({ mode }) => ({
plugins: [
react(),
rewriteComfyImports({ isDev: mode === "development" }),
copyLocales()
],
publicDir: "public", // Explicitly set public directory
build: {
emptyOutDir: true,
rollupOptions: {
// Don't bundle ComfyUI scripts - they will be loaded from the ComfyUI server
external: ['/scripts/app.js', '/scripts/api.js'],
input: {
main: path.resolve(__dirname, 'src/main.tsx'),
},
output: {
// Output to the dist/example_ext directory
dir: '../dist',
entryFileNames: 'example_ext/[name].js',
chunkFileNames: 'example_ext/[name]-[hash].js',
assetFileNames: 'example_ext/[name][extname]',
// Split React into a separate vendor chunk for better caching
manualChunks: {
'vendor': ['react', 'react-dom'],
}
}
}
}
}));
+54
View File
@@ -0,0 +1,54 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
interface RewriteComfyImportsOptions {
isDev: boolean;
}
// Plugin to correctly handle the ComfyUI scripts in development mode
const rewriteComfyImports = ({ isDev }: RewriteComfyImportsOptions) => {
return {
name: "rewrite-comfy-imports",
resolveId(source: string) {
if (!isDev) {
return;
}
if (source === "/scripts/app.js") {
return "http://127.0.0.1:8188/scripts/app.js";
}
if (source === "/scripts/api.js") {
return "http://127.0.0.1:8188/scripts/api.js";
}
return null;
},
};
};
export default defineConfig(({ mode }) => ({
plugins: [
react(),
rewriteComfyImports({ isDev: mode === "development" })
],
build: {
emptyOutDir: true,
rollupOptions: {
// Don't bundle ComfyUI scripts - they will be loaded from the ComfyUI server
external: ['/scripts/app.js', '/scripts/api.js'],
input: {
main: path.resolve(__dirname, 'src/main.tsx'),
},
output: {
// Output to the dist/example_ext directory
dir: '../dist',
entryFileNames: 'example_ext/[name].js',
chunkFileNames: 'example_ext/[name]-[hash].js',
assetFileNames: 'example_ext/[name][extname]',
// Split React into a separate vendor chunk for better caching
manualChunks: {
'vendor': ['react', 'react-dom'],
}
}
}
}
}))