Skip to content

RFC: Standardize how custom node extensions declare and load JS library dependencies #10701

@christian-byrne

Description

@christian-byrne

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

  1. Backend (server.py): The /extensions endpoint uses glob("**/*.js") to find every JS file in all WEB_DIRECTORY paths
  2. Frontend (extensionService.ts): loadExtensions() calls import() on every URL returned — no filtering, no manifest, no opt-in
  3. 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.pyweb.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Custom NodeIssue caused by custom nodesPublic APIAffects or interacts with the public API surface (affecting custom node or extension authors)enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions