diff --git a/packages/g6-extension-solid/README.md b/packages/g6-extension-solid/README.md
new file mode 100644
index 00000000000..df38f0487a8
--- /dev/null
+++ b/packages/g6-extension-solid/README.md
@@ -0,0 +1,118 @@
+## SolidJS extension for G6
+
+
+
+This extension allows you to define G6 node by SolidJS component and JSX syntax with fine-grained reactivity.
+
+## Features
+
+- **Familiar JSX syntax**: Write components using JSX just like React, but with SolidJS's reactive primitives
+- **Fine-grained reactivity**: Unlike React's virtual DOM, SolidJS uses signals for surgical DOM updates
+- **No re-renders**: Component props are reactive signals that update only the parts of the DOM that depend on them
+
+## Usage
+
+1. Install
+
+```bash
+npm install @antv/g6-extension-solid
+```
+
+2. Import and Register
+
+```js
+import { ExtensionCategory, register } from '@antv/g6';
+import { SolidNode } from '@antv/g6-extension-solid';
+
+register(ExtensionCategory.NODE, 'solid-node', SolidNode);
+```
+
+3. Define Node
+
+SolidJS Node:
+
+```jsx
+const SolidNode = (props) => {
+ return
node: {props.id}
;
+};
+```
+
+G Node with SolidJS:
+
+```jsx
+import { Group, Rect, Text } from '@antv/g6-extension-solid';
+
+const GNode = (props) => {
+ return
+
+
+
+};
+```
+
+Reactive Node:
+
+```jsx
+import { createSignal } from 'solid-js';
+
+const ReactiveNode = (props) => {
+ const [count, setCount] = createSignal(0);
+
+ return (
+ setCount(count() + 1)}>
+ Node {props.id}: {count()} clicks
+
+ );
+};
+```
+
+4. Use
+
+Use SolidNode:
+
+```jsx
+const graph = new Graph({
+ // ... other options
+ node: {
+ type: 'solid-node',
+ style: {
+ component: SolidNode,
+ },
+ },
+});
+```
+
+Use GNode:
+
+```jsx
+const graph = new Graph({
+ // ... other options
+ node: {
+ type: 'solid-node',
+ style: {
+ component: GNode,
+ },
+ },
+});
+```
+
+## Key Differences from React Extension
+
+1. **Reactivity Model**: SolidJS uses signals instead of virtual DOM, providing automatic fine-grained updates
+2. **Performance**: No re-renders - only the specific DOM nodes that depend on changed signals are updated
+3. **Props Updates**: Node attributes are automatically reactive through signals, no manual re-rendering needed
+
+## Q&A
+
+1. Difference between SolidNode and GNode
+
+SolidNode is a Solid JSX component that renders to regular DOM, while GNode supports JSX syntax but can only use G tag nodes for SVG/Canvas rendering.
+
+2. How does reactivity work?
+
+The extension automatically creates signals for node attributes. When attributes change in G6, the signals update, and SolidJS reactively updates only the parts of the DOM that depend on those signals.
+
+## Resources
+
+- [SolidJS Documentation](https://www.solidjs.com/)
+- [G6 Custom Nodes](https://g6.antv.antgroup.com/examples/element/custom-node/)
diff --git a/packages/g6-extension-solid/__tests__/.eslintrc b/packages/g6-extension-solid/__tests__/.eslintrc
new file mode 100644
index 00000000000..34f481ae722
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "no-console": "off"
+ }
+}
\ No newline at end of file
diff --git a/packages/g6-extension-solid/__tests__/dataset/euro-cup.json b/packages/g6-extension-solid/__tests__/dataset/euro-cup.json
new file mode 100644
index 00000000000..699cc4ccc54
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/dataset/euro-cup.json
@@ -0,0 +1,114 @@
+{
+ "nodes": [
+ {
+ "id": "50251337",
+ "x": 50,
+ "y": 68,
+ "isTeamA": "1",
+ "player_id": "50251337",
+ "player_shirtnumber": "19",
+ "player_enName": "Justin Kluivert",
+ "player_name": "尤斯廷-克鲁伊维特"
+ },
+ {
+ "id": "50436685",
+ "x": 25,
+ "y": 68,
+ "isTeamA": "1",
+ "player_id": "50436685",
+ "player_shirtnumber": "24",
+ "player_enName": "Antoine Semenyo",
+ "player_name": "塞门约"
+ },
+ {
+ "id": "50204813",
+ "x": 50,
+ "y": 89,
+ "isTeamA": "1",
+ "player_id": "50204813",
+ "player_shirtnumber": "9",
+ "player_enName": "Dominic Solanke",
+ "player_name": "索兰克"
+ },
+ {
+ "id": "50250175",
+ "x": 75,
+ "y": 68,
+ "isTeamA": "1",
+ "player_id": "50250175",
+ "player_shirtnumber": "16",
+ "player_enName": "Marcus Tavernier",
+ "player_name": "塔韦尼耶"
+ },
+ {
+ "id": "50213675",
+ "x": 65,
+ "y": 48,
+ "isTeamA": "1",
+ "player_id": "50213675",
+ "player_shirtnumber": "4",
+ "player_enName": "Lewis Cook",
+ "player_name": "刘易斯-库克"
+ },
+ {
+ "id": "50186648",
+ "x": 35,
+ "y": 48,
+ "isTeamA": "1",
+ "player_id": "50186648",
+ "player_shirtnumber": "10",
+ "player_enName": "Ryan Christie",
+ "player_name": "克里斯蒂"
+ },
+ {
+ "id": "50279448",
+ "x": 38,
+ "y": 28,
+ "isTeamA": "1",
+ "player_id": "50279448",
+ "player_shirtnumber": "6",
+ "player_enName": "Chris Mepham",
+ "player_name": "迈帕姆"
+ },
+ {
+ "id": "50061646",
+ "x": 15,
+ "y": 28,
+ "isTeamA": "1",
+ "player_id": "50061646",
+ "player_shirtnumber": "15",
+ "player_enName": "Adam Smith",
+ "player_name": "亚当-史密斯"
+ },
+ {
+ "id": "50472140",
+ "x": 62,
+ "y": 28,
+ "isTeamA": "1",
+ "player_id": "50472140",
+ "player_shirtnumber": "27",
+ "player_enName": "Ilya Zabarnyi",
+ "player_name": "扎巴尔尼"
+ },
+ {
+ "id": "50544346",
+ "x": 85,
+ "y": 28,
+ "isTeamA": "1",
+ "player_id": "50544346",
+ "player_shirtnumber": "3",
+ "player_enName": "Milos Kerkez",
+ "player_name": "科尔克兹"
+ },
+ {
+ "id": "50062598",
+ "x": 50,
+ "y": 7,
+ "isTeamA": "1",
+ "player_id": "50062598",
+ "player_shirtnumber": "1",
+ "player_enName": "Neto",
+ "player_name": "内托"
+ }
+ ]
+}
diff --git a/packages/g6-extension-solid/__tests__/demos/euro-cup.tsx b/packages/g6-extension-solid/__tests__/demos/euro-cup.tsx
new file mode 100644
index 00000000000..726ac9dc932
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/demos/euro-cup.tsx
@@ -0,0 +1,118 @@
+/* @jsx preserve */
+/* @jsxImportSource solid-js */
+import { ExtensionCategory, register } from '@antv/g6';
+import { SolidNode } from '@antv/g6-extension-solid';
+import styled from 'styled-components';
+import data from '../dataset/euro-cup.json';
+import { Graph } from '../graph';
+
+const Player = styled.div`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+const Shirt = styled.div`
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+
+ img {
+ width: 40px;
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+`;
+
+const Number = styled.div`
+ color: #fff;
+ font-family: 'DingTalk-JinBuTi';
+ font-size: 10px;
+ top: 20px;
+ left: 15px;
+ z-index: 1;
+ margin-top: 16px;
+ margin-left: -2px;
+`;
+
+const Label = styled.div`
+ max-width: 120px;
+ padding: 0 8px;
+ color: #fff;
+ font-size: 10px;
+ background-image: url('https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*s2csQ48M0AkAAAAAAAAAAAAADsvfAQ/original');
+ background-repeat: no-repeat;
+ background-size: cover;
+ display: flex;
+ justify-content: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+const PlayerNode = ({ playerInfo }: any) => {
+ const { isTeamA, player_shirtnumber, player_name } = playerInfo;
+ return (
+
+
+
+ {player_shirtnumber}
+
+
+
+ );
+};
+
+register(ExtensionCategory.NODE, 'solid-node', SolidNode);
+
+export const EuroCup = () => {
+ return (
+
+
d.x * 3.5,
+ y: (d: any) => d.y * 3.5,
+ fill: 'transparent',
+ component: (data: any) => ,
+ },
+ },
+ plugins: [
+ {
+ type: 'background',
+ width: '480px',
+ height: '720px',
+ backgroundImage:
+ 'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EmPXQLrX2xIAAAAAAAAAAAAADmJ7AQ/original)',
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: 'contain',
+ opacity: 1,
+ },
+ ],
+ }}
+ />
+
+ );
+};
diff --git a/packages/g6-extension-solid/__tests__/demos/graph.tsx b/packages/g6-extension-solid/__tests__/demos/graph.tsx
new file mode 100644
index 00000000000..a0d93b6f6b6
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/demos/graph.tsx
@@ -0,0 +1,26 @@
+/* @jsx preserve */
+/* @jsxImportSource solid-js */
+import { Graph } from '../graph';
+
+export const G6Graph = () => {
+ return (
+ {
+ console.log('render');
+ }}
+ onDestroy={() => {
+ console.log('destroy');
+ }}
+ />
+ );
+};
diff --git a/packages/g6-extension-solid/__tests__/demos/index.tsx b/packages/g6-extension-solid/__tests__/demos/index.tsx
new file mode 100644
index 00000000000..8718de31421
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/demos/index.tsx
@@ -0,0 +1,4 @@
+export * from './euro-cup';
+export * from './graph';
+export * from './performance-diagnosis';
+export * from './react-node';
diff --git a/packages/g6-extension-solid/__tests__/demos/performance-diagnosis.tsx b/packages/g6-extension-solid/__tests__/demos/performance-diagnosis.tsx
new file mode 100644
index 00000000000..84732ad68d1
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/demos/performance-diagnosis.tsx
@@ -0,0 +1,154 @@
+/* @jsx preserve */
+/* @jsxImportSource solid-js */
+import { BugOutlined } from '@ant-design/icons';
+import type { EdgeData, Element, GraphData, GraphOptions, IPointerEvent, NodeData } from '@antv/g6';
+import { ExtensionCategory, HoverActivate, idOf, register } from '@antv/g6';
+import { Flex, Typography } from 'antd';
+import { type JSX, createSignal, createEffect } from 'solid-js';
+import { Graph } from '../graph';
+
+const { Text } = Typography;
+
+const ACTIVE_COLOR = '#f6c523';
+const COLOR_MAP: Record = {
+ 'pre-inspection': '#3fc1c9',
+ problem: '#8983f3',
+ inspection: '#f48db4',
+ solution: '#ffaa64',
+};
+
+class HoverElement extends HoverActivate {
+ protected getActiveIds(event: IPointerEvent) {
+ const { model, graph } = this.context;
+ const elementId = event.target.id;
+ const { targetType: elementType } = event;
+
+ const ids = [elementId];
+ if (elementType === 'edge') {
+ const edge = model.getEdgeDatum(elementId);
+ ids.push(edge.source, edge.target);
+ } else if (elementType === 'node') {
+ ids.push(...model.getRelatedEdgesData(elementId).map(idOf));
+ }
+
+ graph.frontElement(ids);
+
+ return ids;
+ }
+}
+
+register(ExtensionCategory.BEHAVIOR, 'hover-element', HoverElement);
+
+const Node = ({ data }: { data: NodeData }) => {
+ const { text, type } = data.data as { text: string; type: string };
+
+ const isHovered = data.states?.includes('active');
+ const isSelected = data.states?.includes('selected');
+ const color = isHovered ? ACTIVE_COLOR : COLOR_MAP[type];
+
+ const containerStyle: JSX.CSSProperties = {
+ width: '100%',
+ height: '100%',
+ background: color,
+ border: `3px solid ${color}`,
+ borderRadius: '16px',
+ cursor: 'pointer',
+ };
+
+ if (isSelected) {
+ Object.assign(containerStyle, { border: `3px solid #000` });
+ }
+
+ return (
+
+
+ {type === 'problem' && }
+ {text}
+
+
+ );
+};
+
+export const PerformanceDiagnosis = () => {
+ const [data, setData] = useState();
+
+ createEffect(() => {
+ fetch('https://assets.antv.antgroup.com/g6/performance-diagnosis.json')
+ .then((res) => res.json())
+ .then(setData);
+ }, []);
+
+ const options: GraphOptions = {
+ data,
+ animation: false,
+ width: 800,
+ height: 600,
+ autoFit: 'view',
+ node: {
+ type: 'react',
+ style: (d: NodeData) => {
+ const style: NodeData['style'] = {
+ component: ,
+ ports: [{ placement: 'top' }, { placement: 'bottom' }],
+ };
+
+ const size = {
+ 'pre-inspection': [240, 120],
+ problem: [200, 120],
+ inspection: [330, 100],
+ solution: [200, 120],
+ }[d.data!.type as string] || [200, 80];
+
+ Object.assign(style, {
+ size,
+ dx: -size[0] / 2,
+ dy: -size[1] / 2,
+ });
+ return style;
+ },
+ state: {
+ active: {
+ halo: false,
+ },
+ selected: {
+ halo: false,
+ },
+ },
+ },
+ edge: {
+ type: 'polyline',
+ style: {
+ lineWidth: 3,
+ radius: 20,
+ stroke: '#8b9baf',
+ endArrow: true,
+ labelText: (d: EdgeData) => d.data!.text as string,
+ labelFill: '#8b9baf',
+ labelFontWeight: 600,
+ labelBackground: true,
+ labelBackgroundFill: '#f8f8f8',
+ labelBackgroundOpacity: 1,
+ labelBackgroundLineWidth: 3,
+ labelBackgroundStroke: '#8b9baf',
+ labelPadding: [1, 10],
+ labelBackgroundRadius: 4,
+ router: {
+ type: 'orth',
+ },
+ },
+ state: {
+ active: {
+ stroke: ACTIVE_COLOR,
+ labelBackgroundStroke: ACTIVE_COLOR,
+ halo: false,
+ },
+ },
+ },
+ layout: {
+ type: 'antv-dagre',
+ },
+ behaviors: ['zoom-canvas', 'drag-canvas', 'hover-element', 'click-select'],
+ };
+
+ return ;
+};
diff --git a/packages/g6-extension-solid/__tests__/demos/solid-node.tsx b/packages/g6-extension-solid/__tests__/demos/solid-node.tsx
new file mode 100644
index 00000000000..37ea9037594
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/demos/solid-node.tsx
@@ -0,0 +1,213 @@
+
+import type { Graph as G6Graph, GraphOptions, NodeData } from '@antv/g6';
+import { ExtensionCategory, register } from '@antv/g6';
+import { SolidNode } from '@antv/g6-extension-solid';
+import { createSignal } from 'solid-js';
+import { Graph } from '../graph';
+
+const { Content, Footer } = Layout;
+const { Text } = Typography;
+
+register(ExtensionCategory.NODE, 'solid-node', SolidNode);
+
+type Datum = {
+ name: string;
+ status: 'success' | 'error' | 'warning';
+ type: 'local' | 'remote';
+ url: string;
+};
+
+const Node = ({ data, onChange }: { data: NodeData; onChange?: (value: string) => void }) => {
+ const { status, type } = data.data as Datum;
+
+ return (
+
+
+
+
+ Server
+ {type}
+
+
+
+ {data.id}
+
+
+ *URL:
+
+ {
+ const url = event.target.value;
+ onChange?.(url);
+ }}
+ />
+
+
+ );
+};
+
+export const ReactNodeDemo = () => {
+ const graphRef = useRef(null);
+
+ const [form] = Form.useForm();
+ const isValidUrl = (url: string) => {
+ return /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/.test(
+ url,
+ );
+ };
+
+ const [options, setOptions] = createSignal({
+ data: {
+ nodes: [
+ {
+ id: 'local-server-1',
+ data: { status: 'success', type: 'local', url: 'http://localhost:3000' },
+ style: { x: 50, y: 50 },
+ },
+ {
+ id: 'remote-server-1',
+ data: { status: 'warning', type: 'remote' },
+ style: { x: 350, y: 50 },
+ },
+ ],
+ edges: [{ source: 'local-server-1', target: 'remote-server-1' }],
+ },
+ node: {
+ type: 'react',
+ style: {
+ size: [240, 100],
+ component: (data: NodeData) => (
+ {
+ setOptions((prev) => {
+ if (!graphRef.current || graphRef.current.destroyed) return prev;
+ const nodes = graphRef.current.getNodeData();
+ const index = nodes.findIndex((node) => node.id === data.id);
+ const node = nodes[index];
+ const datum = {
+ ...node.data,
+ url,
+ status: url === '' ? 'warning' : isValidUrl(url) ? 'success' : 'error',
+ } as Datum;
+ nodes[index] = { ...node, data: datum };
+ return { ...prev, data: { ...prev.data, nodes } };
+ });
+ }}
+ />
+ ),
+ },
+ },
+ behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
+ });
+
+ return (
+
+
+ (graphRef.current = graph)} />
+
+
+
+ );
+};
diff --git a/packages/g6-extension-solid/__tests__/graph.tsx b/packages/g6-extension-solid/__tests__/graph.tsx
new file mode 100644
index 00000000000..07580d33f9a
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/graph.tsx
@@ -0,0 +1,42 @@
+/* @jsx preserve */
+/* @jsxImportSource solid-js */
+import { Graph as G6Graph, GraphOptions } from '@antv/g6';
+import { createEffect, createSignal, onCleanup, onMount } from 'solid-js';
+
+export interface GraphProps {
+ options: GraphOptions;
+ onRender?: (graph: G6Graph) => void;
+ onDestroy?: () => void;
+}
+
+export const Graph = (props: GraphProps) => {
+ const [graph, setGraph] = createSignal();
+ let container!: HTMLDivElement;
+
+ const setGraphOptions = async (options: GraphOptions) => {
+ const g = graph();
+ if (!g) return;
+
+ g.setOptions(options);
+
+ await g.render();
+ props.onRender?.(g);
+ };
+
+ onMount(() => {
+ const graph = new G6Graph({ container });
+ setGraph(graph);
+
+ createEffect(() => {
+ void setGraphOptions(props.options);
+ });
+
+ onCleanup(() => {
+ graph.destroy();
+ props.onDestroy?.();
+ setGraph(undefined);
+ });
+ });
+
+ return ;
+};
diff --git a/packages/g6-extension-solid/__tests__/index.html b/packages/g6-extension-solid/__tests__/index.html
new file mode 100644
index 00000000000..669f88a2312
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ @antv/g6-extension-react
+
+
+
+
+
+
+
diff --git a/packages/g6-extension-solid/__tests__/main.tsx b/packages/g6-extension-solid/__tests__/main.tsx
new file mode 100644
index 00000000000..971dba715a5
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/main.tsx
@@ -0,0 +1,38 @@
+/* @jsx preserve */
+/* @jsxImportSource solid-js */
+import { render } from '@antv/g6-extension-solid';
+import { RouteDefinition, Router, RouteSectionProps, useMatch, useNavigate } from '@solidjs/router';
+import * as demos from './demos';
+
+const App = (props: RouteSectionProps) => {
+ const navigate = useNavigate();
+ const match = useMatch(() => '/*');
+
+ return (
+
+
+ );
+};
+
+const routes = {
+ path: '/',
+ component: App,
+ children: Object.entries(demos).map(([key, Demo]) => ({
+ path: key,
+ component: Demo,
+ })),
+} satisfies RouteDefinition;
+
+const container = document.getElementById('root')!;
+
+render(
+ () => {routes},
+ container,
+);
diff --git a/packages/g6-extension-solid/__tests__/unit/attribute-changed-callback.spec.tsx b/packages/g6-extension-solid/__tests__/unit/attribute-changed-callback.spec.tsx
new file mode 100644
index 00000000000..3fbcfef458e
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/unit/attribute-changed-callback.spec.tsx
@@ -0,0 +1,32 @@
+import { ReactNode } from '../../src';
+
+it('attribute changed callback', () => {
+ const oldComponent = () => test
;
+ const node = new ReactNode({
+ style: {
+ component: oldComponent,
+ },
+ });
+ Object.assign(node, {
+ isConnected: true,
+ ownerDocument: {
+ defaultView: {
+ dispatchEvent: () => {},
+ },
+ },
+ });
+
+ const spy = jest.fn();
+ node.attributeChangedCallback = spy;
+
+ const component = () => test1
;
+
+ try {
+ node.update({ component });
+ } catch (e) {
+ // ignore
+ }
+
+ expect(spy).toHaveBeenCalled();
+ expect(spy).toHaveBeenLastCalledWith('component', oldComponent, component, undefined, undefined);
+});
diff --git a/packages/g6-extension-solid/__tests__/unit/default.spec.ts b/packages/g6-extension-solid/__tests__/unit/default.spec.ts
new file mode 100644
index 00000000000..ce13dc4d36b
--- /dev/null
+++ b/packages/g6-extension-solid/__tests__/unit/default.spec.ts
@@ -0,0 +1,5 @@
+describe('default', () => {
+ it('expect', () => {
+ expect(1).toBe(1);
+ });
+});
diff --git a/packages/g6-extension-solid/jest.config.js b/packages/g6-extension-solid/jest.config.js
new file mode 100644
index 00000000000..29886060fff
--- /dev/null
+++ b/packages/g6-extension-solid/jest.config.js
@@ -0,0 +1,25 @@
+const esm = ['internmap', 'd3-*', 'lodash-es', 'chalk'].map((d) => `_${d}|${d}`).join('|');
+
+module.exports = {
+ transform: {
+ '^.+\\.[tj]sx?$': [
+ '@swc/jest',
+ {
+ jsc: {
+ parser: {
+ syntax: 'typescript',
+ decorators: true,
+ jsx: true,
+ },
+ },
+ },
+ ],
+ },
+ testRegex: '(/__tests__/.*\\.(test|spec))\\.(ts|tsx|js)$',
+ collectCoverageFrom: ['src/**/*.ts'],
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
+ transformIgnorePatterns: [`/node_modules/.pnpm/(?!(${esm}))`],
+ moduleNameMapper: {
+ '@antv/g6': '/../g6/src',
+ },
+};
diff --git a/packages/g6-extension-solid/package.json b/packages/g6-extension-solid/package.json
new file mode 100644
index 00000000000..d4fb2364cb2
--- /dev/null
+++ b/packages/g6-extension-solid/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@antv/g6-extension-solid",
+ "version": "0.2.3",
+ "description": "Using SolidJS Component to Define Your G6 Graph Node",
+ "keywords": [
+ "antv",
+ "g6",
+ "extension",
+ "solid",
+ "solidjs",
+ "node"
+ ],
+ "repository": "https://github.com/antvis/G6.git",
+ "license": "MIT",
+ "author": "Aarebecca",
+ "main": "lib/index.js",
+ "module": "esm/index.js",
+ "types": "lib/index.d.ts",
+ "files": [
+ "src",
+ "esm",
+ "lib",
+ "dist",
+ "README"
+ ],
+ "scripts": {
+ "build": "run-p build:*",
+ "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib -p tsconfig.build.json",
+ "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm -p tsconfig.build.json",
+ "build:umd": "rimraf ./dist && rollup -c",
+ "ci": "run-s lint type-check test build",
+ "dev": "vite",
+ "lint": "eslint ./src __tests__ --quiet && prettier ./src __tests__ --check",
+ "prepublishOnly": "npm run ci",
+ "test": "jest",
+ "type-check": "tsc --noEmit -p tsconfig.test.json"
+ },
+ "dependencies": {
+ "@antv/g": "^6.1.24",
+ "@antv/g-svg": "^2.0.38"
+ },
+ "devDependencies": {
+ "@antv/g6": "workspace:*",
+ "@solidjs/router": "^0.15.3",
+ "solid-js": "^1.8.0"
+ },
+ "peerDependencies": {
+ "@antv/g6": "workspace:^",
+ "solid-js": ">=1.8.0"
+ },
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ }
+}
diff --git a/packages/g6-extension-solid/rollup.config.mjs b/packages/g6-extension-solid/rollup.config.mjs
new file mode 100644
index 00000000000..806938b2d44
--- /dev/null
+++ b/packages/g6-extension-solid/rollup.config.mjs
@@ -0,0 +1,31 @@
+import commonjs from '@rollup/plugin-commonjs';
+import resolve from '@rollup/plugin-node-resolve';
+import terser from '@rollup/plugin-terser';
+import typescript from '@rollup/plugin-typescript';
+import nodePolyfills from 'rollup-plugin-polyfill-node';
+import { visualizer } from 'rollup-plugin-visualizer';
+
+const isBundleVis = !!process.env.BUNDLE_VIS;
+
+export default [
+ {
+ input: 'src/index.ts',
+ output: {
+ file: 'dist/g6-extension-react.min.js',
+ name: 'G6ExtensionReact',
+ format: 'umd',
+ sourcemap: false,
+ inlineDynamicImports: true,
+ },
+ plugins: [
+ nodePolyfills(),
+ resolve(),
+ commonjs(),
+ typescript({
+ tsconfig: 'tsconfig.build.json',
+ }),
+ terser(),
+ ...(isBundleVis ? [visualizer()] : []),
+ ],
+ },
+];
diff --git a/packages/g6-extension-solid/src/index.ts b/packages/g6-extension-solid/src/index.ts
new file mode 100644
index 00000000000..b361ad23031
--- /dev/null
+++ b/packages/g6-extension-solid/src/index.ts
@@ -0,0 +1,3 @@
+export { SolidNode, render, unmount } from './solid-node';
+
+export type { SolidNodeStyleProps } from './solid-node';
diff --git a/packages/g6-extension-solid/src/solid-node/index.ts b/packages/g6-extension-solid/src/solid-node/index.ts
new file mode 100644
index 00000000000..83b89b6c1f8
--- /dev/null
+++ b/packages/g6-extension-solid/src/solid-node/index.ts
@@ -0,0 +1,4 @@
+export { SolidNode } from './node';
+export { render, unmount } from './render';
+
+export type { SolidNodeStyleProps } from './node';
diff --git a/packages/g6-extension-solid/src/solid-node/node.tsx b/packages/g6-extension-solid/src/solid-node/node.tsx
new file mode 100644
index 00000000000..f733b5b07f8
--- /dev/null
+++ b/packages/g6-extension-solid/src/solid-node/node.tsx
@@ -0,0 +1,53 @@
+import type { DisplayObjectConfig, HTMLStyleProps as GHTMLStyleProps } from '@antv/g';
+import type { BaseNodeStyleProps, HTMLStyleProps } from '@antv/g6';
+import { HTML } from '@antv/g6';
+import type { Component, JSX } from 'solid-js';
+import { render, unmount } from './render';
+
+export interface SolidNodeStyleProps extends BaseNodeStyleProps {
+ /**
+ * SolidJS 组件
+ *
+ * SolidJS component
+ */
+ component: Component;
+}
+
+export class SolidNode extends HTML {
+
+
+ protected getKeyStyle(attributes: Required): GHTMLStyleProps {
+ return { ...super.getKeyStyle(attributes) };
+ }
+
+ constructor(options: DisplayObjectConfig) {
+ super(options as any);
+ }
+
+ public update(attr?: Partial | undefined): void {
+ super.update(attr);
+ }
+
+ public connectedCallback() {
+ super.connectedCallback();
+ const { component } = this.attributes as unknown as SolidNodeStyleProps;
+ // component 已经被回调机制自动创建为 SolidNode
+ // component has been automatically created as SolidNode by the callback mechanism
+ render(component, this.getDomElement());
+ }
+
+ public attributeChangedCallback(name: any, oldValue: any, newValue: any) {
+ super.attributeChangedCallback(name, oldValue, newValue);
+ if (name === 'component' && oldValue !== newValue) {
+ render(
+ (this.attributes as unknown as SolidNodeStyleProps).component,
+ this.getDomElement(),
+ );
+ }
+ }
+
+ public destroy(): void {
+ unmount(this.getDomElement());
+ super.destroy();
+ }
+}
diff --git a/packages/g6-extension-solid/src/solid-node/render.ts b/packages/g6-extension-solid/src/solid-node/render.ts
new file mode 100644
index 00000000000..2ce93285a36
--- /dev/null
+++ b/packages/g6-extension-solid/src/solid-node/render.ts
@@ -0,0 +1,41 @@
+import type { Component } from 'solid-js';
+import { render as solidRender } from 'solid-js/web';
+
+// Track disposal functions for cleanup
+const MARK = '__solid_dispose__';
+
+type ContainerType = (Element | DocumentFragment) & {
+ [MARK]?: () => void;
+};
+
+/**
+ * 渲染 SolidJS 组件
+ *
+ * Render SolidJS component
+ * @param component - SolidJS 组件 | SolidJS component
+ * @param container - 容器 | Container
+ */
+export function render(component: Component, container: ContainerType) {
+ // Clean up any existing component first
+ if (container[MARK]) {
+ container[MARK]();
+ delete container[MARK];
+ }
+
+ // Render the new component and store the disposal function
+ const dispose = solidRender(component, container);
+ container[MARK] = dispose;
+}
+
+/**
+ * 卸载 SolidJS 组件
+ *
+ * Unmount SolidJS component
+ * @param container - 容器 | Container
+ */
+export function unmount(container: ContainerType) {
+ if (container[MARK]) {
+ container[MARK]();
+ delete container[MARK];
+ }
+}
diff --git a/packages/g6-extension-solid/tsconfig.build.json b/packages/g6-extension-solid/tsconfig.build.json
new file mode 100644
index 00000000000..5790ea55085
--- /dev/null
+++ b/packages/g6-extension-solid/tsconfig.build.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "paths": {}
+ },
+ "include": ["src/**/*"],
+ "extends": "./tsconfig.json"
+}
diff --git a/packages/g6-extension-solid/tsconfig.json b/packages/g6-extension-solid/tsconfig.json
new file mode 100644
index 00000000000..d032e9fe892
--- /dev/null
+++ b/packages/g6-extension-solid/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "jsx": "react-jsx",
+ "outDir": "lib",
+ "paths": {
+ "@antv/g6": ["../g6/src/index.ts"],
+ "@antv/g6-extension-react": ["./src/index.ts"]
+ }
+ },
+ "extends": "../../tsconfig.json",
+ "include": ["src/**/*", "__tests__/**/*"]
+}
diff --git a/packages/g6-extension-solid/tsconfig.test.json b/packages/g6-extension-solid/tsconfig.test.json
new file mode 100644
index 00000000000..77883aa5748
--- /dev/null
+++ b/packages/g6-extension-solid/tsconfig.test.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "paths": {
+ "@antv/g6": ["../g6/src/index.ts"],
+ "@antv/g6-extension-react": ["./src/index.ts"]
+ },
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*", "__tests__/**/*", "../g6/src/layouts/hierarchy.d.ts"],
+ "extends": "./tsconfig.json"
+}
diff --git a/packages/g6-extension-solid/vite.config.js b/packages/g6-extension-solid/vite.config.js
new file mode 100644
index 00000000000..47ccc263509
--- /dev/null
+++ b/packages/g6-extension-solid/vite.config.js
@@ -0,0 +1,17 @@
+import path from 'path';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ root: './__tests__',
+ server: {
+ port: 8082,
+ open: '/',
+ },
+ plugins: [{ name: 'isolation' }],
+ resolve: {
+ alias: {
+ '@antv/g6': path.resolve(__dirname, '../g6/src'),
+ '@antv/g6-extension-react': path.resolve(__dirname, './src'),
+ },
+ },
+});