Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "{{cookiecutter.project_slug}}/react-extension-template"]
path = {{cookiecutter.project_slug}}/react-extension-template
url = https://github.com/Comfy-Org/ComfyUI-React-Extension-Template
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

An opinionated way to develop ComfyUI custom nodes. Uses cookiecutter to scaffold a python project.

This template helps you
This template helps you

## Usage

Expand All @@ -31,6 +31,13 @@ This template helps you
cd <your-project-name>
git init
```
- Run `cookiecutter` with this as local template:
```bash
git clone https://github.com/Comfy-Org/cookiecutter-comfy-extension
cd cookiecutter-comfy-extension
git submodule update --init --recursive
cookiecutter .
```

## Features

Expand Down
12 changes: 10 additions & 2 deletions cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
"full_name": "",
"email": "[email protected]",
"github_username": "your_github_username",
"frontend_type": ["no", "react", "js"],
"project_name": "My Custom Nodepack",
"project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_').replace('comfyui_', '').replace('comfy_', '') }}",
"project_short_description": "A collection of custom nodes for ComfyUI",
"version": "0.0.1",
"open_source_license": ["GNU General Public License v3", "MIT license", "BSD license", "ISC license", "Apache Software License 2.0", "Not open source"],
"include_web_directory_for_custom_javascript": false,
"__gh_slug": "{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}",
"__include_web": "{{ cookiecutter.include_web_directory_for_custom_javascript }}"
"_copy_without_render": ["*.tsx", "*.ts", "*.yml"],
"__prompts__": {
"frontend_type": {
"__prompt__": "Do you want to write frontend components?",
"no": "No",
"react": "Yes, using React",
"js": "Yes, my own way"
}
}
}
103 changes: 100 additions & 3 deletions hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def init_git():
["git", "branch", "-M", "main"],
["git", "remote", "add", "origin", "https://github.com/{{cookiecutter.__gh_slug}}.git"],
]

for command in commands:
try:
subprocess.run(command, check=True)
Expand All @@ -23,12 +23,109 @@ def init_git():
return True


def replace_text_in_files(files_to_replace, replacements):
"""Replace text in specified files."""
for file_path in files_to_replace:
file_path = Pth(file_path)
if file_path.exists() and file_path.is_file():
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()

for old_text, new_text in replacements.items():
content = content.replace(old_text, new_text)

with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)

print(f"✓ Replaced text in: {file_path}")
except Exception as e:
print(f"× Failed to replace text in {file_path}: {e}")
else:
print(f"× File not found: {file_path}")


def setup_project_structure():
frontend_type = "{{cookiecutter.frontend_type}}"

if frontend_type == "no" or frontend_type == "js":
if Pth("custom-nodes-template").exists():
for item in Pth("custom-nodes-template").iterdir():
target = Pth(item.name)
if item.is_file():
shutil.copy2(item, target)
else:
if target.exists():
shutil.rmtree(target)
shutil.copytree(item, target)
print(f"✓ Copied directory: {item} -> {target}")

if Pth("react-extension-template").exists():
shutil.rmtree("react-extension-template")
print("✓ Removed react-extension-template directory")

elif frontend_type == "react":
if Pth("react-extension-template").exists():
print("✓ Found react-extension-template directory")
for item in Pth("react-extension-template").iterdir():
# Skip git-related files when copying from react template
if item.name in [".git", ".gitmodules", ".gitignore"]:
continue

target = Pth(item.name)
if item.is_file():
shutil.copy2(item, target)
print(f"✓ Copied file: {item} -> {target}")
else:
if target.exists():
shutil.rmtree(target)
shutil.copytree(item, target, ignore=shutil.ignore_patterns('.git', '.gitmodules', ".gitignore"))
print(f"✓ Copied directory: {item} -> {target}")

files_to_replace = [
"README.md",
"ui/package.json",
]

replacements = {
"ComfyUI React Extension Template": "{{cookiecutter.project_name}}",
"ComfyUI-React-Extension-Template": "{{cookiecutter.project_slug}}",
"comfyui-example-react-extension": "{{cookiecutter.project_slug}}",
"Comfy-Org": "{{cookiecutter.github_username}}",
"MIT": "{{cookiecutter.open_source_license}}",
'"version": "0.1.0"': '"version": "{{cookiecutter.version}}"',
}

replace_text_in_files(files_to_replace, replacements)

if Pth("custom-nodes-template").exists():
shutil.rmtree("custom-nodes-template")
print("✓ Removed custom-nodes-template directory")

if Pth("common").exists():
print("✓ Found common directory")
for item in Pth("common").iterdir():
if item.is_file():
shutil.copy2(item, ".")
print(f"✓ Copied common file: {item}")

for template_dir in ["common", "custom-nodes-template", "react-extension-template"]:
if Pth(template_dir).exists():
shutil.rmtree(template_dir)
print(f"✓ Cleaned up: {template_dir}")

if __name__ == "__main__":
setup_project_structure()

if "{{cookiecutter.open_source_license}}" == "Not open source":
Pth("LICENSE").unlink()

if "{{cookiecutter.__include_web}}" == "False":
shutil.rmtree("web")
frontend_type = "{{cookiecutter.frontend_type}}"

if frontend_type == "no":
web_dir = Pth("web")
if web_dir.exists():
shutil.rmtree("web")

if init_git():
print("✓ Git repository initialized and remote configured")
Expand Down
77 changes: 70 additions & 7 deletions hooks/pre_gen_project.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,76 @@
import re
import sys
import subprocess
import os
from pathlib import Path

def init_submodules():
"""Initialize and update git submodules if .gitmodules exists."""
commands = [
["git", "submodule", "init"],
["git", "submodule", "update", "--recursive"]
]

MODULE_REGEX = r'^[_a-zA-Z][_a-zA-Z0-9]+$'
user_home = Path.home()
template_dir = user_home / ".cookiecutters" / "cookiecutter-comfy-extension"

module_name = '{{ cookiecutter.project_slug}}'
for command in commands:
try:
result = subprocess.run(
command,
check=True,
capture_output=True,
text=True,
cwd=template_dir
)
print(f"✓ Executed: {' '.join(command)}")
if result.stdout.strip():
print(f" Output: {result.stdout.strip()}")

if not re.match(MODULE_REGEX, module_name):
print('ERROR: The project slug (%s) is not a valid Python module name. '
'Please do not use a - and use _ instead' % module_name)
#Exit to cancel project
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"× Failed to execute: {' '.join(command)}")
print(f" Error: {e}")
if e.stderr:
print(f" Stderr: {e.stderr}")
print(" Continuing despite submodule error...")
return False
except FileNotFoundError:
print("× Git not found in PATH")
print(" Please ensure Git is installed and available")
return False

print("✓ Submodules initialized successfully")
return True


def validate_project_name():
"""Validate that the project slug is a valid Python module name."""
MODULE_REGEX = r'^[_a-zA-Z][_a-zA-Z0-9]+$'
module_name = '{{ cookiecutter.project_slug}}'

if not re.match(MODULE_REGEX, module_name):
print('ERROR: The project slug (%s) is not a valid Python module name. '
'Please do not use a - and use _ instead' % module_name)
return False

print(f"✓ Project slug '{module_name}' is valid")
return True


if __name__ == "__main__":
template_source = "{{ cookiecutter._template }}"
frontend_type = "{{cookiecutter.frontend_type}}"

if template_source.startswith("gh:") and frontend_type == "react":
submodule_success = init_submodules()

if not submodule_success:
print("⚠️ Warning: Submodule initialization failed, but continuing...")
sys.exit(1)

name_valid = validate_project_name()

if not name_valid:
sys.exit(1)

print("=== Pre-generation setup complete ===")
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,17 @@ 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/
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies = [

]



[project.optional-dependencies]
dev = [
"bump-my-version",
Expand All @@ -35,6 +37,9 @@ homepage = "https://github.com/{{cookiecutter.__gh_slug}}"
PublisherId = "{{cookiecutter.github_username}}"
DisplayName = "{{cookiecutter.project_name}}"
Icon = ""
{% if cookiecutter.frontend_type == 'react' -%}
includes = ["dist/"]
{%- endif %}

[tool.setuptools.package-data]
"*" = ["*.*"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ recursive-exclude * *.py[co]

recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif

{% if cookiecutter.__include_web -%}
{% if cookiecutter.frontend_type == 'js' -%}

graft src/{{cookiecutter.project_slug}}/web

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
{% if cookiecutter.__include_web -%}
{% if cookiecutter.frontend_type == 'js' -%}
"WEB_DIRECTORY",
{%- endif %}
]
Expand All @@ -15,6 +15,6 @@
from .src.{{cookiecutter.project_slug}}.nodes import NODE_CLASS_MAPPINGS
from .src.{{cookiecutter.project_slug}}.nodes import NODE_DISPLAY_NAME_MAPPINGS

{% if cookiecutter.__include_web -%}
{% if cookiecutter.frontend_type == 'js' -%}
WEB_DIRECTORY = "./web"
{%- endif %}
1 change: 1 addition & 0 deletions {{cookiecutter.project_slug}}/react-extension-template