StackGraph


a.ts

export const a = "a"

export const aaa = () => a

b.tsx

import { a, aaa } from "./a.ts"
export { a as exportedA }

export const Comp = () => <div>{a}</div>
export const CompAAA = () => <div>{aaa()}</div>

// unrelated to a and should be ignored
export const Unrelated = () => <div>Unrelated</div>

c.tsx

import { Comp } from "./b.tsx"

// a variable of same name is in a.ts but
// should be treated as different variable
export const a = "1234"

export const Page = () => <div><Comp/></div>

d.tsx

import { exportedA, Unrelated } from "./b.tsx"
import { Page } from "./c.tsx"

export const InnerImport = () => <>
        <Page/>
        <Unrelated/>
    </>
export const AliasedImport = () => <div>{exportedA}</div>

main.ts

import type {
	Project,
	SourceFile,
} from "https://deno.land/x/ts_morph@21.0.1/mod.ts"

import { Stream } from "https://deno.land/x/rimbu@1.2.0/stream/mod.ts"
import {
	getAllDecls,
	getGraph,
	parseVSCodeURI,
} from "https://raw.githubusercontent.com/daangn/stackgraph/main/graph/mod.ts"

import { exampleSrc } from "../graph/_example_project.ts"
import { colorNode } from "./main.ts"
import { inMemoryProject, withSrc } from "../graph/_project.ts"

// 1. ts-morph 프로젝트 생성 (https://ts-morph.com/setup/)
const project: Project = inMemoryProject()

// 2. 프로젝트에 소스 파일 추가 (https://ts-morph.com/setup/adding-source-files)
const files: Record<string, SourceFile> = withSrc(project)(exampleSrc)

// 3. 모든 선언 가져오기
const decls = Stream.fromObjectValues(files).flatMap(getAllDecls).toArray()

// 4. 그래프 생성하기
const graph = getGraph(decls)

// 5. 그래프 노드와 링크를 JSON으로 저장하기
// (https://github.com/vasturiano/force-graph/blob/master/example/basic/index.html)
const nodes = graph.streamNodes().map(parseVSCodeURI).map(colorNode).toArray()

const links = graph.streamConnections()
	.map(([source, target]) => ({
		source: parseVSCodeURI(source).uri,
		target: parseVSCodeURI(target).uri,
	}))
	.toArray()

await Deno.writeTextFile(
	import.meta.dirname + "/assets/data/components.json",
	JSON.stringify({ links, nodes }, null, 2),
)

components.js

// deno-lint-ignore-file

import ForceGraph from "https://esm.sh/force-graph@1.43.4"

const graphDom = document.querySelector("#graph")
if (!graphDom) throw new Error("Root dom not found")

const data = await fetch("./assets/data/components.json")
	.then((res) => res.json())

const Graph = ForceGraph()(graphDom)
	.graphData(data)
	.nodeId("uri")
	.nodeLabel("fullPath")
	.linkDirectionalArrowLength(4)
	// 컴포넌트명을 노드에 표시
	.nodeCanvasObject((node, ctx, globalScale) => {
		const label = node.name
		const fontSize = 16 / globalScale
		ctx.font = `${fontSize}px Sans-Serif`

		const bgWidth = ctx.measureText(label).width + fontSize * 0.2
		const bgHeight = fontSize * 1.2

		ctx.fillStyle = node.color
		ctx.fillRect(node.x - bgWidth / 2, node.y - bgHeight / 2, bgWidth, bgHeight)

		ctx.textAlign = "center"
		ctx.textBaseline = "middle"
		ctx.fillStyle = node.textColor
		ctx.fillText(label, node.x, node.y)

		node.bgWidth = bgWidth
		node.bgHeight = bgHeight
	})
	.nodePointerAreaPaint((node, color, ctx) => {
		ctx.fillStyle = color
		ctx.fillRect(
			node.x - node.bgWidth / 2,
			node.y - node.bgHeight / 2,
			node.bgWidth,
			node.bgHeight,
		)
	})

// 화면 크기에 맞춰 그래프 크기 조정
Graph.width(graphDom.clientWidth).height(graphDom.clientHeight)
globalThis.addEventListener("resize", () => {
	Graph.width(graphDom.clientWidth).height(graphDom.clientHeight)
	console.log(graphDom.clientWidth)
})

index.html

<div id="graph" style="height:40vh" ></div>
<script type="module" src="./assets/components.js" ></script>