@ts-graphviz/react 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +1 -1
  3. package/README.md +25 -63
  4. package/example/example.tsx +14 -12
  5. package/lib/react.cjs +457 -0
  6. package/lib/react.d.ts +230 -0
  7. package/lib/react.js +457 -0
  8. package/package.json +55 -65
  9. package/src/components/Digraph.spec.tsx +18 -0
  10. package/src/components/Digraph.tsx +41 -0
  11. package/src/components/Edge.tsx +17 -0
  12. package/src/components/Graph.spec.tsx +18 -0
  13. package/src/components/Graph.tsx +36 -0
  14. package/src/components/GraphPortal.tsx +22 -0
  15. package/src/components/Node.spec.tsx +101 -0
  16. package/src/components/Node.tsx +21 -0
  17. package/src/components/Subgraph.tsx +30 -0
  18. package/src/contexts/CurrentGraph.ts +5 -0
  19. package/src/contexts/GraphContainer.ts +5 -0
  20. package/src/contexts/GraphMap.ts +5 -0
  21. package/src/contexts/GraphvizContext.ts +11 -0
  22. package/src/hooks/__tests__/wrapper.tsx +58 -0
  23. package/src/hooks/useCurrentGraph.spec.ts +56 -0
  24. package/src/hooks/useCurrentGraph.ts +18 -0
  25. package/src/hooks/useDigraph.spec.ts +15 -0
  26. package/src/hooks/useDigraph.ts +33 -0
  27. package/src/hooks/useEdge.spec.ts +46 -0
  28. package/src/hooks/useEdge.ts +35 -0
  29. package/src/hooks/useGraph.spec.ts +15 -0
  30. package/src/hooks/useGraph.ts +33 -0
  31. package/src/hooks/useGraphAttributes.ts +32 -0
  32. package/src/hooks/useGraphContainer.spec.ts +45 -0
  33. package/src/hooks/useGraphContainer.ts +10 -0
  34. package/src/hooks/useGraphMap.ts +7 -0
  35. package/src/hooks/useGraphvizContext.spec.ts +26 -0
  36. package/src/hooks/useGraphvizContext.ts +16 -0
  37. package/src/hooks/useHasAttributes.ts +16 -0
  38. package/src/hooks/useHasComment.ts +8 -0
  39. package/src/hooks/useNode.spec.ts +16 -0
  40. package/src/hooks/useNode.ts +29 -0
  41. package/src/hooks/useRenderedID.spec.tsx +28 -0
  42. package/src/hooks/useRenderedID.ts +6 -0
  43. package/src/hooks/useSubgraph.spec.ts +41 -0
  44. package/src/hooks/useSubgraph.ts +39 -0
  45. package/src/react.ts +16 -0
  46. package/src/reconciler.ts +272 -0
  47. package/src/render.test.tsx +71 -0
  48. package/src/render.ts +142 -0
  49. package/src/renderHTMLLike.ts +9 -0
  50. package/src/renderId.ts +9 -0
  51. package/src/types.ts +107 -0
  52. package/tsconfig.json +8 -0
  53. package/typedoc.json +5 -0
  54. package/vite.config.ts +32 -0
  55. package/.all-contributorsrc +0 -47
  56. package/.devcontainer/Dockerfile +0 -16
  57. package/.devcontainer/devcontainer.json +0 -35
  58. package/.eslintignore +0 -2
  59. package/lib/index.d.ts +0 -368
  60. package/lib/index.js +0 -1
  61. package/lib/index.mjs +0 -1
  62. package/rollup.config.js +0 -55
@@ -0,0 +1,26 @@
1
+ // @vitest-environment jsdom
2
+ import { renderHook } from '@testing-library/react';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { context } from './__tests__/wrapper.js';
5
+ import { useGraphvizContext } from './useGraphvizContext.js';
6
+
7
+ describe('useGraphvizContext', () => {
8
+ test('returns {}', () => {
9
+ const { result } = renderHook(() => useGraphvizContext(), {
10
+ wrapper: context(),
11
+ });
12
+ expect(result.current).toStrictEqual({});
13
+ });
14
+
15
+ test('An error occurs when called outside the GraphvizContext', () => {
16
+ expect(() => {
17
+ renderHook(() => useGraphvizContext(), {
18
+ // Don't use context() here
19
+ // wrapper: context(),
20
+ });
21
+ }).toThrowErrorMatchingInlineSnapshot(`
22
+ [Error: Cannot call useGraphvizContext outside GraphvizContext.
23
+ Basically, you need to use the render function provided by @ts-graphviz/react.]
24
+ `);
25
+ });
26
+ });
@@ -0,0 +1,16 @@
1
+ import type React from 'react';
2
+ import { useContext } from 'react';
3
+ import type { GraphBaseModel } from 'ts-graphviz';
4
+ import { type Context, GraphvizContext } from '../contexts/GraphvizContext.js';
5
+
6
+ export function useGraphvizContext<T extends GraphBaseModel>(): Context<T> {
7
+ const context = useContext(
8
+ GraphvizContext as unknown as React.Context<Context<T>>,
9
+ );
10
+ if (context === null) {
11
+ throw Error(
12
+ 'Cannot call useGraphvizContext outside GraphvizContext.\nBasically, you need to use the render function provided by @ts-graphviz/react.',
13
+ );
14
+ }
15
+ return context;
16
+ }
@@ -0,0 +1,16 @@
1
+ import { useEffect } from 'react';
2
+ import type {
3
+ AttributeKey,
4
+ AttributesObject,
5
+ HasAttributes,
6
+ } from 'ts-graphviz';
7
+
8
+ export function useHasAttributes<T extends AttributeKey>(
9
+ target: HasAttributes<T>,
10
+ attributes: AttributesObject<T>,
11
+ ): void {
12
+ useEffect(() => {
13
+ target.attributes.clear();
14
+ target.attributes.apply(attributes);
15
+ }, [target, attributes]);
16
+ }
@@ -0,0 +1,8 @@
1
+ import { useEffect } from 'react';
2
+ import type { HasComment } from 'ts-graphviz';
3
+
4
+ export function useHasComment(target: HasComment, comment?: string): void {
5
+ useEffect(() => {
6
+ target.comment = comment;
7
+ }, [target, comment]);
8
+ }
@@ -0,0 +1,16 @@
1
+ // @vitest-environment jsdom
2
+ import { renderHook } from '@testing-library/react';
3
+ import { Node } from 'ts-graphviz';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { digraph } from './__tests__/wrapper.js';
7
+ import { useNode } from './useNode.js';
8
+
9
+ describe('useNode', () => {
10
+ it('returns Node instance', () => {
11
+ const { result } = renderHook(() => useNode('hoge'), {
12
+ wrapper: digraph(),
13
+ });
14
+ expect(result.current).toBeInstanceOf(Node);
15
+ });
16
+ });
@@ -0,0 +1,29 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import type { NodeModel } from 'ts-graphviz';
3
+ import type { NodeOptions } from '../types.js';
4
+ import { useCurrentGraph } from './useCurrentGraph.js';
5
+ import { useHasAttributes } from './useHasAttributes.js';
6
+ import { useHasComment } from './useHasComment.js';
7
+
8
+ /**
9
+ * `useNode` is a hook that creates an instance of Node
10
+ * according to the object given by props.
11
+ */
12
+ export function useNode(id: string, options: NodeOptions = {}): NodeModel {
13
+ const { comment, ...attributes } = options;
14
+ const cluster = useCurrentGraph();
15
+ const node = useMemo(() => {
16
+ const n = cluster.createNode(id);
17
+ n.attributes.apply(attributes);
18
+ n.comment = comment;
19
+ return n;
20
+ }, [cluster, id, attributes, comment]);
21
+ useHasComment(node, comment);
22
+ useHasAttributes(node, attributes);
23
+ useEffect(() => {
24
+ return (): void => {
25
+ cluster.removeNode(node);
26
+ };
27
+ }, [cluster, node]);
28
+ return node;
29
+ }
@@ -0,0 +1,28 @@
1
+ // @vitest-environment jsdom
2
+ import { renderHook } from '@testing-library/react';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { context } from './__tests__/wrapper.js';
5
+ import { useRenderedID } from './useRenderedID.js';
6
+
7
+ describe('useRenderedID', () => {
8
+ it('should return the same value as the input, when input is string', () => {
9
+ const { result } = renderHook(() => useRenderedID('foo'), {
10
+ wrapper: context(),
11
+ });
12
+ expect(result.current).toBe('foo');
13
+ });
14
+
15
+ it('should return the same value as the input, when input is HTMLLike string', () => {
16
+ const { result } = renderHook(() => useRenderedID('<<b>bold</b>>'), {
17
+ wrapper: context(),
18
+ });
19
+ expect(result.current).toBe('<<b>bold</b>>');
20
+ });
21
+
22
+ it('should return the HTMLLike string, when input is JSX', () => {
23
+ const { result } = renderHook(() => useRenderedID(<dot:b>bold</dot:b>), {
24
+ wrapper: context(),
25
+ });
26
+ expect(result.current).toBe('<<b>bold</b>>');
27
+ });
28
+ });
@@ -0,0 +1,6 @@
1
+ import { type ReactElement, useMemo } from 'react';
2
+ import { renderId } from '../renderId.js';
3
+
4
+ export function useRenderedID(id?: ReactElement | string): string | undefined {
5
+ return useMemo(() => renderId(id), [id]);
6
+ }
@@ -0,0 +1,41 @@
1
+ // @vitest-environment jsdom
2
+ import { renderHook } from '@testing-library/react';
3
+ import { Subgraph } from 'ts-graphviz';
4
+ import { describe, expect, it } from 'vitest';
5
+ import {
6
+ digraph,
7
+ digraphInSubgraph,
8
+ graph,
9
+ graphInSubgraph,
10
+ } from './__tests__/wrapper.js';
11
+ import { useSubgraph } from './useSubgraph.js';
12
+
13
+ describe('useSubgraph', () => {
14
+ it('returns Subgraph instance in digraph warper', () => {
15
+ const { result } = renderHook(() => useSubgraph(), {
16
+ wrapper: digraph(),
17
+ });
18
+ expect(result.current).toBeInstanceOf(Subgraph);
19
+ });
20
+
21
+ it('returns Subgraph instance in graph warper', () => {
22
+ const { result } = renderHook(() => useSubgraph(), {
23
+ wrapper: graph(),
24
+ });
25
+ expect(result.current).toBeInstanceOf(Subgraph);
26
+ });
27
+
28
+ it('returns Subgraph instance in graphInSubgraph warper', () => {
29
+ const { result } = renderHook(() => useSubgraph(), {
30
+ wrapper: graphInSubgraph(),
31
+ });
32
+ expect(result.current).toBeInstanceOf(Subgraph);
33
+ });
34
+
35
+ it('returns Subgraph instance in digraphInSubgraph warper', () => {
36
+ const { result } = renderHook(() => useSubgraph(), {
37
+ wrapper: digraphInSubgraph(),
38
+ });
39
+ expect(result.current).toBeInstanceOf(Subgraph);
40
+ });
41
+ });
@@ -0,0 +1,39 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import { type GraphBaseModel, Subgraph, type SubgraphModel } from 'ts-graphviz';
3
+ import type { SubgraphOptions } from '../types.js';
4
+ import { useCurrentGraph } from './useCurrentGraph.js';
5
+ import { useGraphAttributes } from './useGraphAttributes.js';
6
+ import { useGraphvizContext } from './useGraphvizContext.js';
7
+ import { useHasComment } from './useHasComment.js';
8
+
9
+ /**
10
+ * `useSubgraph` is a hook that creates an instance of Subgraph
11
+ * according to the object given by props.
12
+ */
13
+ export function useSubgraph(props: SubgraphOptions = {}): SubgraphModel {
14
+ const { id, comment, edge, node, graph, ...attributes } = props;
15
+ const context = useGraphvizContext();
16
+ const cluster = useCurrentGraph();
17
+ const subgraph = useMemo(() => {
18
+ const g = new Subgraph(id);
19
+ if (cluster !== null) {
20
+ cluster.addSubgraph(g);
21
+ } else if (!context.container) {
22
+ context.container = g as GraphBaseModel;
23
+ }
24
+ g.comment = comment;
25
+ g.apply(attributes);
26
+ g.attributes.node.apply(node ?? {});
27
+ g.attributes.edge.apply(edge ?? {});
28
+ g.attributes.graph.apply(graph ?? {});
29
+ return g;
30
+ }, [context, cluster, id, comment, edge, node, graph, attributes]);
31
+ useHasComment(subgraph, comment);
32
+ useGraphAttributes(subgraph, attributes, { edge, node, graph });
33
+ useEffect(() => {
34
+ return (): void => {
35
+ cluster.removeSubgraph(subgraph);
36
+ };
37
+ }, [cluster, subgraph]);
38
+ return subgraph;
39
+ }
package/src/react.ts ADDED
@@ -0,0 +1,16 @@
1
+ export * from './types.js';
2
+ export * from './hooks/useGraphContainer.js';
3
+ export * from './hooks/useCurrentGraph.js';
4
+ export * from './hooks/useDigraph.js';
5
+ export * from './hooks/useGraph.js';
6
+ export * from './hooks/useSubgraph.js';
7
+ export * from './hooks/useEdge.js';
8
+ export * from './hooks/useNode.js';
9
+ export * from './components/Graph.js';
10
+ export * from './components/Digraph.js';
11
+ export * from './components/Subgraph.js';
12
+ export * from './components/Node.js';
13
+ export * from './components/Edge.js';
14
+ export * from './components/GraphPortal.js';
15
+ export * from './renderHTMLLike.js';
16
+ export * from './render.js';
@@ -0,0 +1,272 @@
1
+ import type { FC } from 'react';
2
+ import ReactReconciler from 'react-reconciler';
3
+
4
+ type Type = FC;
5
+ type Props = any;
6
+ type Container = Record<string, never>;
7
+
8
+ type Instance = any;
9
+ type TextInstance = any;
10
+ type SuspenseInstance = any;
11
+ type HydratableInstance = any;
12
+ type PublicInstance = any;
13
+ type HostContext = any;
14
+ type UpdatePayload = any;
15
+ type ChildSet = any;
16
+ type TimeoutHandle = any;
17
+ type NoTimeout = any;
18
+
19
+ type OpaqueHandle = ReactReconciler.Fiber;
20
+
21
+ export class HostConfig
22
+ implements
23
+ ReactReconciler.HostConfig<
24
+ Type,
25
+ Props,
26
+ Container,
27
+ Instance,
28
+ TextInstance,
29
+ SuspenseInstance,
30
+ HydratableInstance,
31
+ PublicInstance,
32
+ HostContext,
33
+ UpdatePayload,
34
+ ChildSet, // TODO Placeholder for undocumented API
35
+ TimeoutHandle,
36
+ NoTimeout
37
+ >
38
+ {
39
+ preparePortalMount(containerInfo: Container): void {
40
+ // NoOp
41
+ }
42
+
43
+ scheduleTimeout(fn: (...args: unknown[]) => unknown, delay?: number) {
44
+ // NoOp
45
+ }
46
+
47
+ cancelTimeout(id: any): void {
48
+ // NoOp
49
+ }
50
+
51
+ queueMicrotask(fn: () => void): void {
52
+ // NoOp
53
+ }
54
+
55
+ cloneInstance?: any;
56
+
57
+ cloneFundamentalInstance?: any;
58
+
59
+ createContainerChildSet?: any;
60
+
61
+ appendChildToContainerChildSet?: any;
62
+
63
+ finalizeContainerChildren?: any;
64
+
65
+ replaceContainerChildren?: any;
66
+
67
+ cloneHiddenInstance?: any;
68
+
69
+ cloneHiddenTextInstance?: any;
70
+
71
+ public now = Date.now;
72
+
73
+ public setTimeout = setTimeout;
74
+
75
+ public clearTimeout = clearTimeout;
76
+
77
+ public noTimeout: NoTimeout = -1; // TODO
78
+
79
+ // Temporary workaround for scenario where multiple renderers concurrently
80
+ // render using the same context objects. E.g. React DOM and React ART on the
81
+ // same page. DOM is the primary renderer; ART is the secondary renderer.
82
+ public isPrimaryRenderer = false;
83
+
84
+ public supportsMutation = false;
85
+
86
+ public supportsPersistence = false;
87
+
88
+ public supportsHydration = false;
89
+
90
+ public getPublicInstance(instance: Instance | TextInstance): PublicInstance {
91
+ return instance;
92
+ }
93
+
94
+ public getRootHostContext(rootContainerInstance: Container): HostContext {
95
+ return {};
96
+ }
97
+
98
+ public getChildHostContext(
99
+ parentHostContext: HostContext,
100
+ type: Type,
101
+ rootContainerInstance: Container,
102
+ ): HostContext {
103
+ return parentHostContext;
104
+ }
105
+
106
+ public prepareForCommit(
107
+ containerInfo: Container,
108
+ ): Record<string, any> | null {
109
+ return null;
110
+ }
111
+
112
+ public resetAfterCommit(containerInfo: Container): void {
113
+ // containerInfo.setRoot
114
+ }
115
+
116
+ /**
117
+ * Create component instance
118
+ */
119
+ public createInstance(
120
+ type: Type,
121
+ props: Props,
122
+ rootContainerInstance: Container,
123
+ hostContext: HostContext,
124
+ internalInstanceHandle: OpaqueHandle,
125
+ ): Instance {
126
+ // NoOp
127
+ return type(props);
128
+ }
129
+
130
+ public appendInitialChild(
131
+ parentInstance: Instance,
132
+ child: Instance | TextInstance,
133
+ ): void {
134
+ parentInstance.appendChild(child);
135
+ }
136
+
137
+ public finalizeInitialChildren(
138
+ parentInstance: Instance,
139
+ type: Type,
140
+ props: Props,
141
+ rootContainerInstance: Container,
142
+ hostContext: HostContext,
143
+ ): boolean {
144
+ return false;
145
+ }
146
+
147
+ public prepareUpdate(
148
+ instance: Instance,
149
+ type: Type,
150
+ oldProps: Props,
151
+ newProps: Props,
152
+ rootContainerInstance: Container,
153
+ hostContext: HostContext,
154
+ ): null | UpdatePayload {
155
+ return {};
156
+ }
157
+
158
+ public shouldSetTextContent(type: Type, props: Props): boolean {
159
+ return false;
160
+ }
161
+
162
+ public shouldDeprioritizeSubtree(type: Type, props: Props): boolean {
163
+ return false;
164
+ }
165
+
166
+ public createTextInstance(
167
+ text: string,
168
+ rootContainerInstance: Container,
169
+ hostContext: HostContext,
170
+ internalInstanceHandle: OpaqueHandle,
171
+ ): TextInstance {
172
+ return text;
173
+ }
174
+
175
+ public scheduleDeferredCallback(
176
+ callback: () => any,
177
+ options?: { timeout: number },
178
+ ): any {
179
+ // NoOp
180
+ }
181
+
182
+ public cancelDeferredCallback(callbackID: any): void {
183
+ // NoOp
184
+ }
185
+
186
+ // -------------------
187
+ // Mutation
188
+ // (optional)
189
+ // -------------------
190
+ public appendChild(
191
+ parentInstance: Instance,
192
+ child: Instance | TextInstance,
193
+ ): void {
194
+ // NoOp
195
+ if (parentInstance.appendChild) {
196
+ parentInstance.appendChild(child);
197
+ }
198
+ }
199
+
200
+ public appendChildToContainer(
201
+ container: Container,
202
+ child: Instance | TextInstance,
203
+ ): void {
204
+ // if (container.appendChild) {
205
+ // container.appendChild(child);
206
+ // }
207
+ }
208
+
209
+ public commitTextUpdate(
210
+ textInstance: TextInstance,
211
+ oldText: string,
212
+ newText: string,
213
+ ): void {
214
+ // NoOp
215
+ }
216
+
217
+ public commitMount(
218
+ instance: Instance,
219
+ type: Type,
220
+ newProps: Props,
221
+ internalInstanceHandle: OpaqueHandle,
222
+ ): void {
223
+ // NoOp
224
+ }
225
+
226
+ public commitUpdate(
227
+ instance: Instance,
228
+ updatePayload: UpdatePayload,
229
+ type: Type,
230
+ oldProps: Props,
231
+ newProps: Props,
232
+ internalInstanceHandle: OpaqueHandle,
233
+ ): void {
234
+ // NoOp
235
+ }
236
+
237
+ public insertBefore(
238
+ parentInstance: Instance,
239
+ child: Instance | TextInstance,
240
+ beforeChild: Instance | TextInstance,
241
+ ): void {
242
+ // NoOp
243
+ }
244
+
245
+ public insertInContainerBefore(
246
+ container: Container,
247
+ child: Instance | TextInstance,
248
+ beforeChild: Instance | TextInstance,
249
+ ): void {
250
+ // NoOp
251
+ }
252
+
253
+ public removeChild(
254
+ parentInstance: Instance,
255
+ child: Instance | TextInstance,
256
+ ): void {
257
+ // NoOp
258
+ }
259
+
260
+ public removeChildFromContainer(
261
+ container: Container,
262
+ child: Instance | TextInstance,
263
+ ): void {
264
+ // NoOp
265
+ }
266
+
267
+ public resetTextContent(instance: Instance): void {
268
+ // NoOp
269
+ }
270
+ }
271
+
272
+ export const reconciler = ReactReconciler(new HostConfig());
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Digraph } from './components/Digraph.js';
3
+ import { Edge } from './components/Edge.js';
4
+ import { Graph } from './components/Graph.js';
5
+ import { Node } from './components/Node.js';
6
+ import { Subgraph } from './components/Subgraph.js';
7
+ import { renderToDot } from './render.js';
8
+
9
+ describe('renderToDot', () => {
10
+ it('should render a digraph', () => {
11
+ expect(
12
+ renderToDot(
13
+ <Digraph>
14
+ <Node id="a" label="Node A" />
15
+ <Node id="b" label={<dot:b>bold</dot:b>} />
16
+ <Subgraph id="cluster_x" label="Cluster X">
17
+ <Node id="c" label="Node C" />
18
+ </Subgraph>
19
+ <Edge targets={['a', 'b']} />
20
+ </Digraph>,
21
+ ),
22
+ ).toMatchInlineSnapshot(`
23
+ "digraph {
24
+ "a" [
25
+ label = "Node A";
26
+ ];
27
+ "b" [
28
+ label = <<b>bold</b>>;
29
+ ];
30
+ subgraph "cluster_x" {
31
+ label = "Cluster X";
32
+ "c" [
33
+ label = "Node C";
34
+ ];
35
+ }
36
+ "a" -> "b";
37
+ }"
38
+ `);
39
+ });
40
+
41
+ it('should render a graph', () => {
42
+ expect(
43
+ renderToDot(
44
+ <Graph>
45
+ <Node id="a" label="Node A" />
46
+ <Node id="b" label="Node B" />
47
+ <Subgraph id="cluster_x" label="Cluster X">
48
+ <Node id="c" label="Node C" />
49
+ </Subgraph>
50
+ <Edge targets={['a', 'b']} />
51
+ </Graph>,
52
+ ),
53
+ ).toMatchInlineSnapshot(`
54
+ "graph {
55
+ "a" [
56
+ label = "Node A";
57
+ ];
58
+ "b" [
59
+ label = "Node B";
60
+ ];
61
+ subgraph "cluster_x" {
62
+ label = "Cluster X";
63
+ "c" [
64
+ label = "Node C";
65
+ ];
66
+ }
67
+ "a" -- "b";
68
+ }"
69
+ `);
70
+ });
71
+ });