fix: render node search highlights as text - #14580
Conversation
🎭 Playwright: ✅ 1782 passed, 0 failed📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 8.21 MB gzip 🔴 +228 BDetailsSummary
Category Glance App Entry Points — 3.64 kB (baseline 3.64 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.29 MB (baseline 1.29 MB) • 🔴 +935 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed / 1 unchanged Views & Navigation — 111 kB (baseline 111 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 12 added / 12 removed / 4 unchanged Panels & Settings — 551 kB (baseline 551 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 15 unchanged User & Accounts — 28.7 kB (baseline 28.7 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed / 3 unchanged Editors & Dialogs — 124 kB (baseline 124 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 6 added / 6 removed / 1 unchanged UI Components — 70 kB (baseline 70 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 9 unchanged Data & Services — 3.45 MB (baseline 3.45 MB) • 🟢 -27 BStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 386 kB (baseline 386 kB) • 🔴 +280 BHelpers, composables, and utility bundles
Status: 18 added / 18 removed / 18 unchanged Vendor & Third-Party — 15.7 MB (baseline 15.7 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 12.7 MB (baseline 12.8 MB) • 🟢 -68 BBundles that do not match a named category
Status: 72 added / 72 removed / 209 unchanged ⚡ Performance Report
✅ No regressions detected. All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-08-02T19:43:54.047Z",
"gitSha": "a8f94e5ea15a6db234a9b0244195be9376c2b8f6",
"branch": "agent/harden-v-html",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2005.6020000000103,
"styleRecalcs": 11,
"styleRecalcDurationMs": 4.898,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 320.136,
"heapDeltaBytes": 5240112,
"heapUsedBytes": 73153764,
"domNodes": 22,
"jsHeapTotalBytes": 19787776,
"scriptDurationMs": 10.446999999999997,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2004.903000000013,
"styleRecalcs": 11,
"styleRecalcDurationMs": 4.579999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 300.31600000000003,
"heapDeltaBytes": 4961732,
"heapUsedBytes": 72883896,
"domNodes": 22,
"jsHeapTotalBytes": 20049920,
"scriptDurationMs": 10.640000000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1707.1789999999964,
"styleRecalcs": 73,
"styleRecalcDurationMs": 18.602,
"layouts": 12,
"layoutDurationMs": 1.7339999999999998,
"taskDurationMs": 540.258,
"heapDeltaBytes": -466148,
"heapUsedBytes": 67334224,
"domNodes": 56,
"jsHeapTotalBytes": 20836352,
"scriptDurationMs": 63.352000000000004,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1686.191000000008,
"styleRecalcs": 72,
"styleRecalcDurationMs": 18.183,
"layouts": 12,
"layoutDurationMs": 1.767,
"taskDurationMs": 540.6260000000001,
"heapDeltaBytes": -14604732,
"heapUsedBytes": 53127164,
"domNodes": -277,
"jsHeapTotalBytes": 21487616,
"scriptDurationMs": 62.32299999999999,
"eventListeners": -146,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1669.8299999999904,
"styleRecalcs": 32,
"styleRecalcDurationMs": 9.834,
"layouts": 6,
"layoutDurationMs": 0.40099999999999997,
"taskDurationMs": 242.60399999999998,
"heapDeltaBytes": 8055388,
"heapUsedBytes": 75767852,
"domNodes": 79,
"jsHeapTotalBytes": 20574208,
"scriptDurationMs": 11.117999999999999,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1669.4120000000225,
"styleRecalcs": 31,
"styleRecalcDurationMs": 9.652000000000001,
"layouts": 6,
"layoutDurationMs": 0.39700000000000013,
"taskDurationMs": 241.526,
"heapDeltaBytes": 8022712,
"heapUsedBytes": 75848300,
"domNodes": 80,
"jsHeapTotalBytes": 20574208,
"scriptDurationMs": 11.091999999999999,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "dom-widget-clipping",
"durationMs": 374.6710000000064,
"styleRecalcs": 13,
"styleRecalcDurationMs": 4.613,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 230.273,
"heapDeltaBytes": -12408744,
"heapUsedBytes": 55477956,
"domNodes": 22,
"jsHeapTotalBytes": 21360640,
"scriptDurationMs": 29.898999999999997,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 369.8070000000371,
"styleRecalcs": 12,
"styleRecalcDurationMs": 4.031,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 229.24499999999998,
"heapDeltaBytes": -12506204,
"heapUsedBytes": 55412796,
"domNodes": 20,
"jsHeapTotalBytes": 21360640,
"scriptDurationMs": 28.808999999999997,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2007.583000000011,
"styleRecalcs": 11,
"styleRecalcDurationMs": 5.0600000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 452.515,
"heapDeltaBytes": 7528944,
"heapUsedBytes": 68225640,
"domNodes": -277,
"jsHeapTotalBytes": 4780032,
"scriptDurationMs": 54.781,
"eventListeners": -142,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-idle",
"durationMs": 2008.7730000000192,
"styleRecalcs": 10,
"styleRecalcDurationMs": 4.804999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 451.34899999999993,
"heapDeltaBytes": 8136608,
"heapUsedBytes": 67330148,
"domNodes": -278,
"jsHeapTotalBytes": 5042176,
"scriptDurationMs": 54.202000000000005,
"eventListeners": -142,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2055.088000000012,
"styleRecalcs": 69,
"styleRecalcDurationMs": 8.422,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 770.55,
"heapDeltaBytes": 21628160,
"heapUsedBytes": 83743620,
"domNodes": -279,
"jsHeapTotalBytes": 5267456,
"scriptDurationMs": 226.26,
"eventListeners": -172,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2058.506999999963,
"styleRecalcs": 68,
"styleRecalcDurationMs": 8.395999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 771.885,
"heapDeltaBytes": 848080,
"heapUsedBytes": 60931808,
"domNodes": -282,
"jsHeapTotalBytes": 5005312,
"scriptDurationMs": 232.975,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3028.2600000000175,
"styleRecalcs": 66,
"styleRecalcDurationMs": 8.616999999999999,
"layouts": 60,
"layoutDurationMs": 4.271999999999999,
"taskDurationMs": 858.106,
"heapDeltaBytes": 21628224,
"heapUsedBytes": 85206224,
"domNodes": 16,
"jsHeapTotalBytes": 7864320,
"scriptDurationMs": 273.18199999999996,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3020.9600000000023,
"styleRecalcs": 67,
"styleRecalcDurationMs": 9.545000000000002,
"layouts": 60,
"layoutDurationMs": 4.482,
"taskDurationMs": 889.1909999999999,
"heapDeltaBytes": 22850888,
"heapUsedBytes": 86518456,
"domNodes": 20,
"jsHeapTotalBytes": 8388608,
"scriptDurationMs": 301.57900000000006,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2014.2529999999965,
"styleRecalcs": 10,
"styleRecalcDurationMs": 4.555999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 449.39300000000003,
"heapDeltaBytes": 8078116,
"heapUsedBytes": 70285480,
"domNodes": -279,
"jsHeapTotalBytes": 4517888,
"scriptDurationMs": 53.016,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 1994.5999999999913,
"styleRecalcs": 10,
"styleRecalcDurationMs": 4.425,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 443.824,
"heapDeltaBytes": 7581880,
"heapUsedBytes": 69346652,
"domNodes": -279,
"jsHeapTotalBytes": 4517888,
"scriptDurationMs": 52.89200000000001,
"eventListeners": -142,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 418.74599999999873,
"styleRecalcs": 47,
"styleRecalcDurationMs": 6.081000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 251.232,
"heapDeltaBytes": -12125280,
"heapUsedBytes": 55863768,
"domNodes": 20,
"jsHeapTotalBytes": 23195648,
"scriptDurationMs": 69.45400000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 409.9499999999807,
"styleRecalcs": 48,
"styleRecalcDurationMs": 6.238,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 244.40200000000002,
"heapDeltaBytes": -11959712,
"heapUsedBytes": 55868888,
"domNodes": 22,
"jsHeapTotalBytes": 22147072,
"scriptDurationMs": 65.273,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-idle",
"durationMs": 1990.179000000012,
"styleRecalcs": 11,
"styleRecalcDurationMs": 4.917,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 343.92100000000005,
"heapDeltaBytes": -21448944,
"heapUsedBytes": 46709936,
"domNodes": -277,
"jsHeapTotalBytes": 20176896,
"scriptDurationMs": 8.944999999999999,
"eventListeners": -146,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1998.9249999999856,
"styleRecalcs": 11,
"styleRecalcDurationMs": 5.306,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 343.123,
"heapDeltaBytes": 4470908,
"heapUsedBytes": 72354240,
"domNodes": 22,
"jsHeapTotalBytes": 20836352,
"scriptDurationMs": 9.915000000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1674.4089999999971,
"styleRecalcs": 77,
"styleRecalcDurationMs": 19.249,
"layouts": 16,
"layoutDurationMs": 2.1339999999999995,
"taskDurationMs": 462.25000000000006,
"heapDeltaBytes": -4797020,
"heapUsedBytes": 63107212,
"domNodes": 65,
"jsHeapTotalBytes": 21098496,
"scriptDurationMs": 45.799,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1670.254,
"styleRecalcs": 77,
"styleRecalcDurationMs": 18.976,
"layouts": 16,
"layoutDurationMs": 2.1350000000000002,
"taskDurationMs": 466.94699999999995,
"heapDeltaBytes": -4270348,
"heapUsedBytes": 63748036,
"domNodes": 65,
"jsHeapTotalBytes": 20312064,
"scriptDurationMs": 46.674,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-transition-enter",
"durationMs": 833.611000000019,
"styleRecalcs": 19,
"styleRecalcDurationMs": 19.041000000000004,
"layouts": 15,
"layoutDurationMs": 7.302999999999999,
"taskDurationMs": 631.0910000000001,
"heapDeltaBytes": 29028828,
"heapUsedBytes": 95095488,
"domNodes": 13673,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 17.27,
"eventListeners": 2369,
"totalBlockingTimeMs": 78,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8079.166000000015,
"styleRecalcs": 250,
"styleRecalcDurationMs": 21.344,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2747.8199999999997,
"heapDeltaBytes": 7454684,
"heapUsedBytes": 68515788,
"domNodes": -277,
"jsHeapTotalBytes": 6316032,
"scriptDurationMs": 769.194,
"eventListeners": -126,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8058.942999999999,
"styleRecalcs": 250,
"styleRecalcDurationMs": 21.312,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2663.806,
"heapDeltaBytes": 9647392,
"heapUsedBytes": 70761244,
"domNodes": -278,
"jsHeapTotalBytes": 6316032,
"scriptDurationMs": 739.2769999999999,
"eventListeners": -126,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 8845.958999999993,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 8801.634,
"heapDeltaBytes": -60368736,
"heapUsedBytes": 166090720,
"domNodes": -8312,
"jsHeapTotalBytes": -15671296,
"scriptDurationMs": 349.63100000000003,
"eventListeners": -16394,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 8599.100999999962,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 8555.466,
"heapDeltaBytes": -34462212,
"heapUsedBytes": 175147820,
"domNodes": -8312,
"jsHeapTotalBytes": -6234112,
"scriptDurationMs": 305.176,
"eventListeners": -16395,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000013,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-pan",
"durationMs": 11032.14399999996,
"styleRecalcs": 66,
"styleRecalcDurationMs": 10.944999999999983,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10984.617,
"heapDeltaBytes": -35112288,
"heapUsedBytes": 173862572,
"domNodes": -8312,
"jsHeapTotalBytes": -8400896,
"scriptDurationMs": 568.832,
"eventListeners": -16393,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 11156.266000000016,
"styleRecalcs": 65,
"styleRecalcDurationMs": 10.980000000000018,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 11122.043,
"heapDeltaBytes": -39766952,
"heapUsedBytes": 159662276,
"domNodes": -8312,
"jsHeapTotalBytes": -10166272,
"scriptDurationMs": 562.963,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 75.68900000001122,
"styleRecalcs": 7,
"styleRecalcDurationMs": 9.704,
"layouts": 3,
"layoutDurationMs": 0.955,
"taskDurationMs": 58.81,
"heapDeltaBytes": 3286204,
"heapUsedBytes": 70277984,
"domNodes": 139,
"jsHeapTotalBytes": 3145728,
"scriptDurationMs": 4.95,
"eventListeners": 35,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 434.7050000000081,
"styleRecalcs": 14,
"styleRecalcDurationMs": 11.507000000000001,
"layouts": 2,
"layoutDurationMs": 0.2800000000000001,
"taskDurationMs": 81.151,
"heapDeltaBytes": -16067372,
"heapUsedBytes": 50784244,
"domNodes": 121,
"jsHeapTotalBytes": 7729152,
"scriptDurationMs": 6.117000000000001,
"eventListeners": 67,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
🌐 Website E2ETip All tests passed.
|
📝 WalkthroughWalkthroughChangesSearch highlighting migration
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant NodeSearchItem
participant HighlightedText
participant highlightQuery
NodeSearchItem->>HighlightedText: pass text and query
HighlightedText->>highlightQuery: create structured text parts
highlightQuery-->>HighlightedText: return highlighted and plain segments
HighlightedText-->>NodeSearchItem: render literal text segments
Possibly related PRs
Suggested labels: Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #14580 +/- ##
=======================================
Coverage 79.14% 79.14%
=======================================
Files 1769 1770 +1
Lines 113307 113316 +9
Branches 35242 35242
=======================================
+ Hits 89678 89687 +9
+ Misses 23092 23087 -5
- Partials 537 542 +5
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 5 files with indirect coverage changes 🚀 New features to boost your workflow:
|
|
Superseded by three intentionally disjoint draft PRs:
Node search has been removed from this work and is left entirely to #14574. The live file set of each replacement PR has an empty intersection with #14574, and the three replacement PRs are mutually file-disjoint. Closing this broader draft to avoid duplicate review and merge conflicts. Verification evidence is attached to each replacement PR. |
|
Reopened as #14789, rebased onto main now that #14574 has landed. Flagging why: when this was split into #14583/#14584/#14585, the searchbox half didn't carry over anywhere. The |
…omfy-Org#14789) Follow-up to Comfy-Org#14574, reviving the work from the closed Comfy-Org#14580 rebased onto current main. Comfy-Org#14574 landed the escape-then-sanitize fix so Cloud could be patched without waiting. It left three things behind, all of which lived in Comfy-Org#14580 and are in no other open PR: - the `v-html` sink in both search components - the `sanitize` parameter, which is a complete bypass when false - a lint guardrail to stop the sink coming back ## Changes `highlightQuery` now returns `{ text, highlighted }[]` instead of an HTML string, and the new `HighlightedText.vue` renders those segments as Vue text nodes. There is no markup to escape, no sanitizer to bypass, and no `v-html` in either the legacy or v2 search UI. `vue/no-v-html` is set to `error` for `src/components/searchbox/**`. Verified it fires: a probe file with `v-html` produces `'v-html' directive can lead to XSS attack`. Removing the string API also drops the `dompurify` dependency from `shared-frontend-utils`. ## Why this rather than keeping the escaping The original defect was not a missing sanitizer — `highlightQuery` already called DOMPurify. It was an early return above the call. Escaping correctly is a property that has to hold at every future edit; not building HTML is a property that holds structurally. ## Testing - 204 tests pass across `formatUtil` and `src/components/searchbox` - eslint clean, oxfmt clean - rebase conflict in `formatUtil.ts` resolved in favour of the segment API, which supersedes the escape-and-sanitize string it replaces ## Note on scope This closes the searchbox sink. Two sibling sinks of the same class are handled in Comfy-Org#14772 (property-name `innerHTML`, and Open Image blob typing) — neither is Vue, so this lint rule does not cover them. Original work by @huang47 in Comfy-Org#14580. --------- Co-authored-by: ShihChi Huang <shh@theonlyperson.com> Co-authored-by: t <t@t.t>
Summary
Render node-search highlights as Vue text nodes instead of injecting generated HTML. This closes the workflow/custom-node-controlled HTML injection path in both the legacy and v2 search UIs.
Related to #14574. This version removes the raw HTML sink and the optional sanitizer bypass instead of relying on sanitization before later string interpolation.
Changes
highlightQuerynow returns text/highlight segments;HighlightedTextrenders them through Vue interpolation in both search implementations.vue/no-v-htmlis an error forsrc/components/searchbox/**/*.vue.dompurifyfrom@comfyorg/shared-frontend-utils; the application still retains DOMPurify for its audited markdown/content sinks.Security audit
git grep -n 'v-html=' origin/main -- 'src/**/*.vue' 'apps/**/*.vue'found 24 directives:NodeSearchItem.vue(2),NodeSearchListItem.vue(2)v-htmlparsed it as markup. Removed in this PR.WidgetTextPreview.vue,WidgetMarkdown.vue,NodeHelpContent.vue,NodePreview.vue,TextPreviewWidget.vue,ReleaseNotificationToast.vue,WhatsNewPopup.vueDOMPurify.sanitize()orrenderMarkdownToHtml(), which sanitizes with DOMPurify. No bypass found.ContentSection(1),FAQSection(2), customer/galleryContactSection(2),DemoTranscript(1),LegalContentSection(2),WhatsIncludedSection(1),ReasonSection(1)ToolPanel.vueSVG map (1), cloud/workspace pricing arrows (2)Branch result: 18 remaining audited directives, 0 in node search. The six removed directives are all four unsafe search sinks plus both hard-coded arrow sinks.
Root cause: sanitizing a string before generating highlight markup creates a fragile trust boundary, and the empty-query branch skipped sanitization entirely. The new path never constructs HTML from node names; Vue escapes every text segment.
Verification
pnpm test:unit packages/shared-frontend-utils/src/formatUtil.test.ts— 84/84 passedpnpm test:unit src/components/searchbox/v2/NodeSearchListItem.test.ts— 29/29 passed, including<img src=x onerror=alert(1)>with an empty querypnpm typecheck— passedpnpm lint— passed with 0 errors (existing warnings only)pnpm knip— passedpnpm build— passed; 5,968 modules transformedpnpm test:unit --reporter=dot— 14,504 passed, 1 unrelated timeout in unchangedonboardingCloudRoutes.test.ts; isolated rerun passed 7/7rg -n --glob '*.vue' 'v-html=' src/components/searchbox— no matchesGitHub CI
Required checks passed on commit
29a7e7207: