Problem
Custom node extensions that need third-party JS libraries (markdown parsers, visualization libraries, code editors) cannot place them in WEB_DIRECTORY because the frontend unconditionally import()s every .js file found there. This forces node packs to use workarounds like registering custom aiohttp static routes (kjweb_async, mtb_async), which break on Cloud.
Root Cause
- Backend (
server.py): The /extensions endpoint uses glob("**/*.js") to find every JS file in all WEB_DIRECTORY paths
- Frontend (
extensionService.ts): loadExtensions() calls import() on every URL returned — no filtering, no manifest, no opt-in
- Vendored libraries (e.g.,
marked.min.js) get executed as ES modules on every page load, causing errors and global pollution
Current Workarounds
| Pack |
Workaround |
How |
| KJNodes |
kjweb_async/ folder |
Custom web.static("/kjweb_async", ...) aiohttp route |
| comfy_mtb |
web_async/ folder |
Custom web.static("/mtb_async", ...) aiohttp route |
Both use a loadScript() helper to inject <script> tags on demand. Both break on Cloud.
Proposed Layered Solution
Short-term (S, in progress)
Expose commonly-needed libraries from the frontend bundle.
marked and DOMPurify are already bundled as direct dependencies. PR #10700 exposes renderMarkdownToHtml() on ExtensionManager, eliminating the need for custom nodes to bundle their own copies.
// Custom node extension can now do:
const html = app.extensionManager.renderMarkdownToHtml(nodeData.description)
Medium-term (S-M)
Static subdirectory convention: Backend treats a subdirectory (e.g., web/libs/ or web/static/) as served-but-not-imported.
# Backend change (~5 lines):
# When building the /extensions response, exclude files under a `libs/` subdirectory
# Files are still served via the existing static route, just not included in the glob
This formalizes what kjweb_async does manually, without requiring custom add_routes() calls. Cloud-compatible because files are served from the standard extension static path.
Long-term (M)
Extension manifest in pyproject.toml: Custom nodes already declare metadata in [tool.comfy]. Extend this to declare which files are extensions vs. static assets:
[tool.comfy]
PublisherId = "kijai"
DisplayName = "KJNodes"
[tool.comfy.web]
extensions = ["web/js/*.js"] # Files to import() as extensions
static = ["web/libs/*"] # Files to serve but not auto-import
This gives the backend and frontend a proper manifest for filtering, without breaking any existing packs (missing manifest = current behavior).
Design Considerations
- Backward compatibility: All solutions should be additive — existing
WEB_DIRECTORY behavior unchanged
- Cloud compatibility: Solutions must work without custom aiohttp routes
- Deduplication: If multiple packs bundle the same library, consider shared asset serving
- Security: Exposed utilities should be safe by default (e.g.,
renderMarkdownToHtml includes DOMPurify sanitization)
References
- KJNodes
kjweb_async pattern: init.py — web.static("/kjweb_async", ...)
- comfy_mtb
web_async pattern: comfy_mtb
- Frontend extension loading:
src/services/extensionService.ts L31-53
- Backend extension serving:
server.py /extensions endpoint
┆Issue is synchronized with this Notion page by Unito
Problem
Custom node extensions that need third-party JS libraries (markdown parsers, visualization libraries, code editors) cannot place them in
WEB_DIRECTORYbecause the frontend unconditionallyimport()s every.jsfile found there. This forces node packs to use workarounds like registering custom aiohttp static routes (kjweb_async,mtb_async), which break on Cloud.Root Cause
server.py): The/extensionsendpoint usesglob("**/*.js")to find every JS file in allWEB_DIRECTORYpathsextensionService.ts):loadExtensions()callsimport()on every URL returned — no filtering, no manifest, no opt-inmarked.min.js) get executed as ES modules on every page load, causing errors and global pollutionCurrent Workarounds
kjweb_async/folderweb.static("/kjweb_async", ...)aiohttp routeweb_async/folderweb.static("/mtb_async", ...)aiohttp routeBoth use a
loadScript()helper to inject<script>tags on demand. Both break on Cloud.Proposed Layered Solution
Short-term (S, in progress)
Expose commonly-needed libraries from the frontend bundle.
markedandDOMPurifyare already bundled as direct dependencies. PR #10700 exposesrenderMarkdownToHtml()onExtensionManager, eliminating the need for custom nodes to bundle their own copies.Medium-term (S-M)
Static subdirectory convention: Backend treats a subdirectory (e.g.,
web/libs/orweb/static/) as served-but-not-imported.This formalizes what
kjweb_asyncdoes manually, without requiring customadd_routes()calls. Cloud-compatible because files are served from the standard extension static path.Long-term (M)
Extension manifest in
pyproject.toml: Custom nodes already declare metadata in[tool.comfy]. Extend this to declare which files are extensions vs. static assets:This gives the backend and frontend a proper manifest for filtering, without breaking any existing packs (missing manifest = current behavior).
Design Considerations
WEB_DIRECTORYbehavior unchangedrenderMarkdownToHtmlincludes DOMPurify sanitization)References
kjweb_asyncpattern: init.py —web.static("/kjweb_async", ...)web_asyncpattern: comfy_mtbsrc/services/extensionService.tsL31-53server.py/extensionsendpoint┆Issue is synchronized with this Notion page by Unito