├── .editorconfig ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── LICENSE ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── favicon.svg ├── src ├── App.css ├── App.tsx ├── components │ ├── Editor.tsx │ ├── Header.tsx │ ├── Panels.tsx │ ├── Preferences.tsx │ ├── Renderer │ │ ├── CustomEdge.tsx │ │ ├── CustomHandle.tsx │ │ ├── ModelNode.tsx │ │ ├── Renderer.tsx │ │ ├── RendererWrapper.tsx │ │ └── index.ts │ ├── Share.tsx │ └── Sidebar.tsx ├── edge-segment-cache.ts ├── examples.ts ├── hooks │ ├── useDebounced.ts │ ├── useFullscreen.ts │ ├── useIsMobile.ts │ └── useMediaQuery.ts ├── index.css ├── lib │ └── parser │ │ ├── ModelParser.test.ts │ │ ├── ModelParser.ts │ │ ├── Parser.test.ts │ │ └── Parser.ts ├── main.tsx ├── monaco-vim.d.ts ├── reactflow.css ├── stores │ ├── documents.ts │ ├── graph.ts │ └── user-options.ts ├── themes │ ├── index.ts │ ├── solarized-dark.json │ ├── solarized-light.json │ ├── vs-dark.json │ └── vs-light.json ├── types.ts ├── utils │ └── svg-export.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | stats.html 27 | coverage 28 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx vitest run 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "printWidth": 110, 7 | "quoteProps": "as-needed", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "always", 11 | "useTabs": false 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TSDiagram 2 | 3 | **TSDiagram** is an online tool that helps you draft diagrams quickly by using TypeScript. 4 | \ 5 | :point_right: https://tsdiagram.com 6 | 7 | ### **Features** 8 | 9 | - Lets you define your data models through **top-level type aliases**, **interfaces**, and **classes**. 10 | - Automatically layouts the nodes in an efficient way. 11 | - ...but if you move one of the nodes manually, it will only auto-layout the other ones. 12 | - Persists the document state in the URL and localStorage. 13 | - Export your diagrams as SVG. 14 | 15 | ### **Roadmap** 16 | 17 | - Function call representation 18 | - Customizable TypeScript context (lib, etc.) 19 | - Bring your own storage (different vendors) 20 | 21 | --- 22 | 23 | > This project is not just a diagramming tool, but also the foundation for a greater code visualization project. 24 | > Imagine flagging types and functions in your code editor and see how they are connected, and how data flows through them. 25 | > That's the end goal, so we'll swap the TS compiler with Tree-sitter in the process. 26 | 27 | --- 28 | 29 | ![TSDiagram Screenshot](https://root.b-cdn.net/tsdiagram/media.png) 30 | 31 | ### Test links 32 | 33 | - [Default example](https://tsdiagram.com/#/N4IgJg9gxgrgtgUwHYBcDOIBcBtUBLMLEACwBsBrAIQEFi4AFAKwEMBGAJwGEEAzU2AO6UAqiAA0IFHhSkERAOoJ+EROJBoIMdlDmYQeVAnY9mOgAQA5CGARngAHSRmzBTGbQp2BgOYBuR84ADswoxG4eXkh+Ae6a2gjhnj7+TmbeCChmiCjMABQAlG4ASghQEOxgADwRPmJmMEjkSBACSAB8Kc7pmVIyCAWJkdGp3WakBuRoA5bWCNgAup1pGWYARqbk441ThTM2C0ujOWiT0wAqzCcHjgC+KY4GKEYm5hcndjG9soPJMVDEeFIYHYyDcb3I11SHhCMDQbns4F4zBgpBQCLMAB8zAjTFIAG4IdFYhGQJCEkCY7EgKDMJA6UiyMAIpZof4IMAohJmcEAZTZHNkLIQaDQeAgSDh3Mu5B5wtF4shXRWeDQAEkkPR2BBvCCRdNVhAILJaSkbo4HoZjKZbLz+ZyPlCcuwUG4ACIhBBLZBgN0ew4rDnsEJipDTJDwVZGf2ZFWcLQg1D6w3GpCm81IR7Pa1Sk6ykUhh3OaHO31PL1IMAAflLnpio0DwfFYYjUbryrQcfYCZQSaNCBNt3TUFIlzQZnkeHIeELZi1EBQ9BCYXcSSi91SZQlnhgUBQ5Vyc4XS5+UXyM+coRVADpD4vQmYALyzw1H0JLM1INsoKw2HZuH9zPM56zhkWhOIqZgfjcagjh4ACy1h4DweDslgrAAOwAAwAEwAGwACwAJwAMzYdhACsrC4Tc8wSLAXbICgrrQPAjGqoQehkFQtAMCwHDcHwggiCANxAA) 34 | - [Interfaces](https://tsdiagram.com/#/N4IgJg9gxgrgtgUwHYBcDOIBcBtUBLMLEAIUgDMBhANwGkBaGgcwE4APATQHcAvAKwCkAqgA0YAQxAAaECjwoANgiIBJVAgBOZMVAQZpaCDHU6ieNZu0IABAEErwADpIrLq2QgRMVtCnVnGANxOAL5OZigaWjq29k6uVojqjAhgXj5+SIEhYeZR1sSxzq4ARmLqXjZBSKFIOREW0RSF8WJeAEoIUBDqYAA86f6StgB8VfHFXgCyYgAOvcRDA5nD2UjhkZZWACLNrq222AC6VTV1G9EAolYIrBFIYGgxjkUupeXevv4nZw3WAGLXW7IB62IYFZ7jMTcNKfTLfWprXKbADivQAKkNBMNdi50l40WNXEh4F5BPD1r8rAAJdGAu4gpaMTFWAC8VmJcGKGmxN3pj1RGKsWJxVgiPhhGSy1SkIHkYh8kwgYDwZDwKSwAEYAOwABgATAA2AAszA1Ro1AA4DXrgodpLB1OpkCgttB4M7lIRMCRyNR6Ew2Fw+EJRBJgkA) 35 | - [Type aliases](https://tsdiagram.com/#/N4IgJg9gxgrgtgUwHYBcDOIBcBtUBLMLEACwCYB2AJQC0BPAMQGYJTSBpALwHEEB1AQ14BRAI4BOEABoQKPCgA2CIgBVaABwQACAILy8-NAgzS0EGACcoSzDPVbtmgLyah58xHOaAZJuAAdJE0gzUMANwRzOVpMTT8QAHd+cyQ8JABzOM0AH1iQCPdzTJy4gDN+FH55OIBuAIBfAJQ7TQAhJx1apEbmgGF2-0Dg-hjtTuDNACMYlrHg8wQoDzAYygWlgB40FEj0yU0AEQA+eoDujQP2rZ206qkQeQMUAFkIMDwSvARCTABGcgAGUgANgALGJSGJyGJGD86gBdaSwNzIFD7aDwFEASW+JAoNAYzFYnB4AmE4hAdSAA) 36 | - [Interfaces and classes extends & implements](https://tsdiagram.com/#/N4IgJg9gxgrgtgUwHYBcDOIBcBtUBLMLEAKQFYBJAIwBEAOAOQC8ANAfSloFkAVAZgCsAwlABijAIwgANCBR4UAGwREAogA8UyMGgAEAMh144AByWJUGGWggwATlGWYQUBQEM0ugII7gOgGYQEJg6aCi2eEgA5gDcOgC+ADpILu66AEI+OpSutgAUAJTBoeFRmSgAFrYQAO46SAi1KrZVefmxcfFJKR46gjoIGlpehiZmyOg6Gb7ZrZm2CCh2SDoJINmMq+2dSEkRmrZ+rg46zD5JOhf+gUVhETFJiUh7CAdHCDoAmmfLlzM3JfckI9nq9jgAtfqDJDaE5ST7fS5ZVyMf53aIPaQgNyhTgQMB4Px4BCETDiADsAAYAEwANgALABOXhUqmkcQ0uIAXRksGa42o0Hg43IJJIFBoDBY7C4fCEogkIDiQA) 37 | 38 | ### Special thanks <3 39 | 40 | - [TypeScript](https://www.typescriptlang.org/) 41 | - [React Flow](https://reactflow.dev) 42 | - [Monaco](https://github.com/microsoft/monaco-editor) 43 | - [elkjs](https://github.com/kieler/elkjs) 44 | - [dom-to-svg](https://github.com/felixfbecker/dom-to-svg) 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | TSDiagram - Diagrams as code with TypeScript 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsdiagram", 3 | "version": "0.0.3", 4 | "repository": "https://github.com/3rd/tsdiagram", 5 | "description": "Create diagrams and plan your code with TypeScript.", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite --open", 9 | "build": "tsc && vite build", 10 | "preview": "vite preview", 11 | "test": "vitest", 12 | "coverage": "vitest run --coverage", 13 | "format": "prettier --write .", 14 | "prepare": "husky" 15 | }, 16 | "lint-staged": { 17 | "*.{json,js,ts,jsx,tsx,html}": [ 18 | "prettier --write --ignore-unknown" 19 | ] 20 | }, 21 | "dependencies": { 22 | "@headlessui/react": "^1.7.19", 23 | "@jspm/core": "^2.0.1", 24 | "@monaco-editor/react": "^4.6.0", 25 | "@radix-ui/react-icons": "^1.3.0", 26 | "@tisoap/react-flow-smart-edge": "^3.0.0", 27 | "classnames": "^2.5.1", 28 | "dom-to-svg": "^0.12.2", 29 | "elkjs": "^0.9.3", 30 | "lodash": "^4.17.21", 31 | "lz-string": "^1.5.0", 32 | "million": "^3.0.6", 33 | "monaco-vim": "^0.4.1", 34 | "nanoid": "^5.0.7", 35 | "path-browserify": "^1.0.1", 36 | "pathfinding": "^0.4.18", 37 | "process": "^0.11.10", 38 | "react": "^18.2.0", 39 | "react-dom": "^18.2.0", 40 | "react-resizable-panels": "^2.0.17", 41 | "reactflow": "^11.11.1", 42 | "source-map-js": "^1.2.0", 43 | "statelift": "^1.0.15", 44 | "ts-morph": "^22.0.0", 45 | "zod": "^3.22.4" 46 | }, 47 | "devDependencies": { 48 | "@types/lodash": "^4.17.0", 49 | "@types/node": "^20.12.7", 50 | "@types/pathfinding": "^0.0.9", 51 | "@types/react": "^18.2.79", 52 | "@types/react-dom": "^18.2.25", 53 | "@vitejs/plugin-react": "^4.3.4", 54 | "@vitest/coverage-v8": "^1.5.0", 55 | "autoprefixer": "^10.4.19", 56 | "husky": "^9.0.11", 57 | "lint-staged": "^15.2.2", 58 | "postcss": "^8.4.38", 59 | "prettier": "^3.2.5", 60 | "rollup-plugin-visualizer": "^5.12.0", 61 | "tailwindcss": "^3.4.3", 62 | "typescript": "^5.4.5", 63 | "vite": "^5.2.9", 64 | "vitest": "^1.5.0", 65 | "web-worker": "^1.3.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { ReactFlowProvider } from "reactflow"; 3 | import { Header } from "./components/Header"; 4 | import { Panels } from "./components/Panels"; 5 | import { Editor } from "./components/Editor"; 6 | import { RendererWrapper } from "./components/Renderer"; 7 | import { Preferences } from "./components/Preferences"; 8 | import { Share } from "./components/Share"; 9 | import { Sidebar } from "./components/Sidebar"; 10 | import { useUserOptions } from "./stores/user-options"; 11 | import "./App.css"; 12 | 13 | function App() { 14 | const [showPreferences, setShowPreferences] = useState(false); 15 | const [showShare, setShowShare] = useState(false); 16 | const options = useUserOptions(); 17 | 18 | const handlePreferencesClick = useCallback(() => { 19 | setShowPreferences((value) => !value); 20 | }, []); 21 | 22 | const handleShareClick = useCallback(() => { 23 | setShowShare((value) => !value); 24 | }, []); 25 | 26 | return ( 27 | 28 |
29 |
30 |
31 | {options.general.sidebarOpen && } 32 | } rendererChildren={} /> 33 |
34 | 35 | 36 |
37 |
38 | ); 39 | } 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /src/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, memo, useCallback, useEffect, useRef } from "react"; 2 | import MonacoEditor, { useMonaco } from "@monaco-editor/react"; 3 | import { InitVimModeResult, initVimMode } from "monaco-vim"; 4 | import { themes } from "../themes"; 5 | import { documentsStore } from "../stores/documents"; 6 | import { useUserOptions } from "../stores/user-options"; 7 | import { useStore } from "statelift"; 8 | 9 | type MonacoMountHandler = ComponentProps["onMount"]; 10 | type IStandaloneCodeEditor = Parameters>[0]; 11 | 12 | const editorOptions: ComponentProps["options"] = { 13 | minimap: { enabled: false }, 14 | renderLineHighlight: "none", 15 | fontSize: 15, 16 | scrollbar: { 17 | vertical: "auto", 18 | horizontal: "auto", 19 | }, 20 | }; 21 | 22 | export const Editor = memo(() => { 23 | const options = useUserOptions(); 24 | const currentDocumentSource = useStore(documentsStore, (state) => state.currentDocument.source); 25 | 26 | const monaco = useMonaco(); 27 | const editorRef = useRef(null); 28 | const vimModeRef = useRef(null); 29 | const vimStatusLineRef = useRef(null); 30 | 31 | const isVimMode = options.editor.editingMode === "vim"; 32 | 33 | const handleSourceChange = useCallback((value: string | undefined) => { 34 | documentsStore.state.setCurrentDocumentSource(value ?? ""); 35 | documentsStore.state.save(); 36 | }, []); 37 | 38 | useEffect(() => { 39 | if (!monaco) return; 40 | const themeConfig = themes[(options.editor.theme as keyof typeof themes) ?? "vsLight"] as Parameters< 41 | typeof monaco.editor.defineTheme 42 | >[1]; 43 | monaco.editor.defineTheme("theme", themeConfig); 44 | monaco.editor.setTheme("theme"); 45 | }, [monaco, options.editor.theme]); 46 | 47 | const handleMount: MonacoMountHandler = (mountedEditor, mountedMonaco) => { 48 | editorRef.current = mountedEditor; 49 | 50 | const compilerOptions = mountedMonaco.languages.typescript.typescriptDefaults.getCompilerOptions(); 51 | compilerOptions.target = mountedMonaco.languages.typescript.ScriptTarget.Latest; 52 | compilerOptions.lib = ["esnext"]; 53 | mountedMonaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions); 54 | mountedEditor.updateOptions({ cursorStyle: isVimMode ? "block" : "line" }); 55 | mountedEditor.getModel()?.updateOptions({ tabSize: 2, indentSize: 2 }); 56 | mountedEditor.focus(); 57 | 58 | if (isVimMode) { 59 | if (!vimStatusLineRef.current) throw new Error("vimStatusLineRef.current is null"); 60 | vimModeRef.current = initVimMode(mountedEditor, vimStatusLineRef.current); 61 | vimModeRef.current.on("vim-mode-change", ({ mode }) => { 62 | if (!editorRef.current) return; 63 | mountedEditor.updateOptions({ cursorStyle: mode === "insert" ? "line" : "block" }); 64 | }); 65 | } 66 | }; 67 | 68 | useEffect(() => { 69 | if (isVimMode) { 70 | if (vimModeRef.current) return; 71 | if (!editorRef.current) return; 72 | if (!vimStatusLineRef.current) return; 73 | vimModeRef.current = initVimMode(editorRef.current, vimStatusLineRef.current); 74 | editorRef.current.updateOptions({ cursorStyle: "block" }); 75 | } else { 76 | vimModeRef.current?.dispose(); 77 | vimModeRef.current = null; 78 | editorRef.current?.updateOptions({ cursorStyle: "line" }); 79 | } 80 | }, [isVimMode]); 81 | 82 | return ( 83 |
84 | 91 | {isVimMode &&
} 92 |
93 | ); 94 | }); 95 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { Share1Icon, GearIcon, ArrowRightIcon, ArrowLeftIcon, FilePlusIcon } from "@radix-ui/react-icons"; 3 | import { useUserOptions } from "../stores/user-options"; 4 | import { documentsStore } from "../stores/documents"; 5 | import classNames from "classnames"; 6 | import { useStore } from "statelift"; 7 | 8 | type HeaderProps = { 9 | onPreferencesClick?: () => void; 10 | onShareClick?: () => void; 11 | }; 12 | 13 | export const Header = memo(({ onPreferencesClick, onShareClick }: HeaderProps) => { 14 | const options = useUserOptions(); 15 | const documentTitle = useStore(documentsStore, (state) => state.currentDocument.title); 16 | 17 | const handleTitleChange = (e: React.ChangeEvent) => { 18 | documentsStore.state.setCurrentDocumentTitle(e.target.value); 19 | documentsStore.state.save(); 20 | }; 21 | 22 | const handleSidebarButtonClick = () => { 23 | options.general.sidebarOpen = !options.general.sidebarOpen; 24 | options.save(); 25 | }; 26 | 27 | const handleNewDocumentClick = () => { 28 | documentsStore.state.create(); 29 | }; 30 | 31 | return ( 32 |
33 | {/* sidebar header */} 34 | {options.general.sidebarOpen && ( 35 |
41 |
42 | Documents 43 | 49 |
50 |
51 | )} 52 | 53 | {/* main wrapper */} 54 |
55 | {/* left */} 56 |
57 | {/* sidebar button */} 58 | 64 | 65 | {/* logo */} 66 |
67 | 68 | TS 69 | 70 | Diagram 71 |