Build canvas lucide SVG without react-dom/server
- Dominant language
- TypeScript
- Stars
- 481
- Forks
- 108
- Avg merge
- 6d 8h
- Merged PRs (30d)
- 5
Description
Remove `react-dom/server` from the client bundle by building the cytoscape lucide SVG string from Lucide geometry data instead of rendering a React element to markup.
## Context
`utils/lucideIcons.ts` produces the canvas SVG string with:
```ts
const { default: Icon } = await dynamicIconImports[iconName]();
return renderToStaticMarkup(createElement(Icon));
```
`renderToStaticMarkup` is the only reason `react-dom/server` (~238 KB raw, ~60–80 KB gzipped) is in the client bundle — verified as the sole import across `packages/graph-explorer/src`. The DOM surfaces no longer need it at all: `VertexSymbolIcon` renders lucide through `DynamicIcon`, so only the cytoscape `background-image` still needs a string.
Every lucide icon module also exports its geometry as plain data:
```js
// dist/esm/icons/plane.mjs
const __iconNode = [["path", { d: "M17.8 19.2 …", key: "1v9wt8" }]];
export { __iconNode, Plane as default };
```
`DynamicIcon` itself reads `__iconNode`, so this is the same data lucide-react renders from.
## Why this was deferred
PR #1777 had a hand-rolled `buildSvgString` + `escapeXmlAttr` and its review removed it — finding **R7, "`buildSvgString` unvalidated tag interpolation"**, resolved as "no longer constructing SVG manually". Reintroducing string concatenation reopens a closed finding, which is a conversation this branch did not need to have.
**Build via `document.createElementNS` + `XMLSerializer`, not string concatenation.** With no interpolation there is no injection surface to validate, so R7 does not apply by construction, and `XMLSerializer` is already used in `core/icons/iconImageUrl.ts`.
## Scope
- Pin `lucide-react` to exact `1.20.0` (currently `^1.20.0`) — `__iconNode` is exported and typed but underscore-prefixed, so treat it as semi-public.
- Add a shape-assertion test so a future bump that changes `__iconNode` fails CI loudly rather than silently rendering blank icons.
- Build the canvas SVG from `__iconNode` + lucide’s `defaultAttributes` via `createElementNS`.
- Drop `renderToStaticMarkup` and confirm `react-dom/server` leaves the bundle.
## Notes from investigation
Across all 1985 icon modules in 1.20.0:
- Only 7 element types: `circle`, `ellipse`, `line`, `path`, `polygon`, `polyline`, `rect`. No `g`.
- Zero icons use `id`, `defs`, gradients, masks, `clipPath`, or `style`, so there is no collision surface.
- All icon-level attribute keys are already lowercase. React’s `key` must be stripped rather than emitted.
- camelCase conversion is needed for exactly three wrapper attributes, all from the fixed `defaultAttributes`: `strokeWidth`, `strokeLinecap`, `strokeLinejoin`. A camelCase attribute emitted into raw SVG is silently ignored by the browser, so this is the main correctness risk.
- 10 icons set their own `fill="currentColor"` (`vault`, `tag`, `tags`, `chart-scatter`, …), so per-element `fill` must be emitted when present.
## Out of scope
Recoloring custom SVG icons — tracked in #2105.
## Related
Follows the icon pipeline rework in #2102. Background in `docs/adr/20260813-icon-registry-not-react-query.md` (see the final consequence).
---
> [!IMPORTANT]
> Internal only — this issue is maintained by the core team and is not accepting external contributions.
Contributor guide
Research direction
Start in packages/graph-explorer/src/utils/lucideIcons.ts and inspect core/icons/iconImageUrl.ts for the existing XMLSerializer usage. Add the requested __iconNode shape assertion, verify the lucide-react pin and bundle dependency changes, and confirm react-dom/server is no longer included while canvas icons still render.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- build-system, frontend
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 20/100