Files
olivv-cs-ComfyUI-FunPack/core/routes.py
T
digital-garbage 835fab7864 core+shell: the bridge, and the first module through it
One declaration does three jobs: renders the panel, carries the defaults, names
the values that come back. A module writes no DOM and reads no DOM.

Validation refuses rather than repairs -- unknown type, a renderer its type does
not have, a label that is not text, a default outside its own bounds, a when
naming a key the module never declared. A module that is wrong does not get a
best-effort panel; it does not load, and says why once.

Ordering is by declared relation and stage, never priority numbers, and a cycle
drops only the modules in it. Compatibility is by trait, never model name.

The shell hands a module { composer, values, on, shell } and nothing else: no
node, no document, no kit internals. It cannot style anything because it is
never given anything that could.

catalogue/hide.html puts one good module and five broken ones through the real
shell and shows the result: one panel, five absent, no placeholders. The lint
fixtures make the scanner prove it catches each rule, so a clean scan means
something.

First slice: modules/system/appearance. Two settings, an optional ui.js, and
clicking Light actually turns the app light.
2026-08-28 19:58:06 +03:00

123 lines
3.9 KiB
Python

"""Route registration on ComfyUI's aiohttp server.
`register()` takes the route table rather than reaching for PromptServer itself,
so the real routes can be mounted on a throwaway app in a test. The handlers are
thin adapters over pure functions in `serve`.
"""
from . import config, log, registry as registry_mod, serve as static
from .contract import CONTRACT_VERSION
from .relations import order
from .traits import split
# Two independent imports, two independent guards: aiohttp is installable on its
# own, and folding them together nulled `web` whenever ComfyUI's `server` was
# absent -- which is every test run.
try:
from aiohttp import web
except Exception:
web = None
try:
from server import PromptServer
except Exception: # not running inside ComfyUI
PromptServer = None
def _respond(served):
if served.status != 200:
return web.Response(status=served.status, headers=served.headers)
return web.Response(
body=served.body,
content_type=served.content_type,
headers=served.headers,
)
def _serve_under(req, root, allowed):
# match_info keeps dot-segments intact for a request as it arrives on the
# wire (aiohttp does not collapse them during matching), so a traversal
# reaches resolve() and is refused there. Note that aiohttp's own TestClient
# normalises the URL before sending -- a test written with it cannot see
# this path at all, which is why test_routes.py uses a raw socket.
return _respond(static.serve(root, req.match_info["tail"], allowed))
# Scanned once at startup. A module list that changed under the UI mid-session
# would mean a panel could refer to something that is no longer there.
_registry = None
def modules(rescan=False):
global _registry
if _registry is None or rescan:
_registry = registry_mod.scan()
return _registry
def manifest(traits=None):
"""What the browser is told: only modules that loaded and validated.
A module that failed is absent from `modules` entirely -- the UI renders
what announced itself, so absence here is what makes it absent on screen.
`failed` is carried alongside for the modules dump, never for rendering.
"""
reg = modules()
specs = list(reg.specs.values())
incompatible = []
if traits is not None:
specs, incompatible = split(specs, traits)
ordered, rejected = order(specs)
return {
"contract": CONTRACT_VERSION,
"modules": [spec.to_manifest() for spec in ordered],
"failed": (
[{"where": where, "why": why} for where, why in reg.failed]
+ [{"where": spec.source, "why": why} for spec, why in rejected]
),
"incompatible": [
{"id": spec.id, "requires": spec.requires} for spec in incompatible
],
}
def register(routes, prefix=None):
"""Attach FunPack's routes to an aiohttp route table."""
P = config.UI_PREFIX if prefix is None else prefix
@routes.get(P + "/api/health")
async def _health(_req):
return web.json_response({"ok": True})
@routes.get(P + "/api/modules")
async def _modules(req):
raw = req.query.get("traits")
traits = [t for t in raw.split(",") if t] if raw is not None else None
return web.json_response(manifest(traits))
@routes.get(P + "/app/{tail:.*}")
async def _app_asset(req):
return _serve_under(req, config.APP_DIR, config.APP_EXTS)
@routes.get(P + "/modules/{tail:.*}")
async def _module_asset(req):
return _serve_under(req, config.MODULES_DIR, config.MODULE_EXTS)
@routes.get(P + "/")
async def _index(_req):
return _respond(static.serve(config.APP_DIR, "index.html", config.APP_EXTS))
@routes.get(P)
async def _index_bare(_req):
raise web.HTTPFound(P + "/")
return routes
if web is not None and PromptServer is not None:
register(PromptServer.instance.routes)
log.note(f"serving the app at {config.UI_PREFIX}/")