-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 813946d
Showing
16 changed files
with
1,241 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
assets | ||
dist | ||
_site |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"editor.defaultFormatter": "denoland.vscode-deno", | ||
"[javascript][typescript][typescriptreact][markdown][json][jsonc]": { | ||
"editor.defaultFormatter": "denoland.vscode-deno" | ||
}, | ||
"deno.config": "./deno.jsonc", | ||
"deno.enable": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# StackGraph | ||
|
||
**StackGraph**는 JSX 컴포넌트 관계도와 메타데이터를 정적 분석해 시각화해요. | ||
|
||
## 시작하기 | ||
|
||
### 데이터 준비하기 | ||
|
||
다음 형태의 JSON 파일을 준비해주세요. | ||
|
||
```ts | ||
export type LabelOption = { | ||
flows: Record<string, { | ||
type: "push" | "replace" | ||
to: string | ||
}[]> | ||
clicks: Record<string, string[]> | ||
shows: Record<string, string[]> | ||
} | ||
``` | ||
### 렌더링하기 | ||
```sh | ||
$ git clone https://github.com/daangn/stackgraph | ||
$ cd stackgraph | ||
|
||
$ deno run -A render/build.ts <data.json 파일 경로> | ||
|
||
$ deno task serve | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import lume from "lume/mod.ts" | ||
|
||
const site = lume({ src: "render" }) | ||
|
||
site.copy("index.html", "index.html") | ||
site.copy("assets", "assets") | ||
|
||
export default site |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"exclude": ["render/assets", "_site"], | ||
"tasks": { | ||
"lume": "echo \"import 'lume/cli.ts'\" | deno run --unstable -A -", | ||
"build": "deno task lume", | ||
"serve": "deno task lume -s" | ||
}, | ||
"fmt": { | ||
"semiColons": false, | ||
"useTabs": true, | ||
"proseWrap": "never" | ||
}, | ||
"compilerOptions": { | ||
"allowJs": false, | ||
"exactOptionalPropertyTypes": true, | ||
"lib": ["deno.window", "dom"], | ||
"jsx": "precompile", | ||
"jsxImportSource": "npm:preact", | ||
"types": ["./main.d.ts", "lume/types.ts"] | ||
}, | ||
"imports": { | ||
"lume/": "https://deno.land/x/[email protected]/" | ||
} | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { RealFileSystemHost } from "https://deno.land/x/[email protected]/common/mod.ts" | ||
|
||
export class FilteredFSHost extends RealFileSystemHost { | ||
constructor(readonly ignore: (path: string) => boolean) { | ||
super() | ||
} | ||
|
||
override fileExists(filePath: string): Promise<boolean> { | ||
if (this.ignore(filePath)) return Promise.resolve(false) | ||
return super.fileExists(filePath) | ||
} | ||
override fileExistsSync(filePath: string): boolean { | ||
if (this.ignore(filePath)) return false | ||
return super.fileExistsSync(filePath) | ||
} | ||
override directoryExists(dirPath: string): Promise<boolean> { | ||
if (this.ignore(dirPath)) return Promise.resolve(false) | ||
return super.directoryExists(dirPath) | ||
} | ||
override directoryExistsSync(dirPath: string): boolean { | ||
if (this.ignore(dirPath)) return false | ||
return super.directoryExistsSync(dirPath) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { | ||
ReferencedSymbol, | ||
ReferencedSymbolEntry, | ||
SyntaxKind, | ||
VariableDeclaration, | ||
} from "https://deno.land/x/[email protected]/mod.ts" | ||
|
||
type Optional<T> = [T] | [] | ||
const toOptional = <T>(x: T | undefined): Optional<T> => (x ? [x] : []) | ||
|
||
export type Graph = Map<VariableDeclaration, VariableDeclaration[]> | ||
|
||
const getTopmostVarDecl = ( | ||
ref: ReferencedSymbolEntry, | ||
): Optional<VariableDeclaration> => { | ||
const decl = ref | ||
.getNode() | ||
.getAncestors() | ||
.findLast((x) => x.getKindName() === "VariableDeclaration") | ||
?.asKind(SyntaxKind.VariableDeclaration) | ||
|
||
return toOptional(decl) | ||
} | ||
|
||
export const getGraph = (vardecl: VariableDeclaration): Graph => { | ||
const graph: Graph = new Map() | ||
|
||
const getReferencedVarDecls = ( | ||
node: VariableDeclaration, | ||
): VariableDeclaration[] => { | ||
const file = node.getSourceFile() | ||
|
||
// TODO: use isDefinition()? | ||
const getActualReferences = ( | ||
symbol: ReferencedSymbol, | ||
): ReferencedSymbolEntry[] => | ||
symbol | ||
.getReferences() | ||
.filter((ref) => | ||
ref.getSourceFile().getFilePath() !== file.getFilePath() | ||
) | ||
.filter((ref) => | ||
ref.getNode().getParent()?.getKindName() !== "ImportClause" | ||
) | ||
|
||
const varDecls = node.findReferences() | ||
.flatMap(getActualReferences) | ||
.flatMap(getTopmostVarDecl) | ||
|
||
graph.set(node, varDecls) | ||
return varDecls | ||
} | ||
|
||
let decls = [vardecl] | ||
while (decls.length > 0) { | ||
decls = decls.flatMap(getReferencedVarDecls) | ||
} | ||
return graph | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { distinct } from "https://deno.land/[email protected]/collections/distinct.ts" | ||
import { | ||
SourceFile, | ||
VariableDeclaration, | ||
} from "https://deno.land/x/[email protected]/mod.ts" | ||
import { getGraph } from "./graph.ts" | ||
import type { Graph } from "./graph.ts" | ||
|
||
export type Relations<A, B> = Record<string, Omit<LinkedVarDecl<A, B>, "decl">> | ||
export type LinkedVarDecl<A, B> = { | ||
decl: VariableDeclaration | ||
links: A | ||
metas: B | ||
} | ||
|
||
export type Metadata = { | ||
logShowEvents: string[] | ||
logClickEvents: string[] | ||
} | ||
|
||
export type Dependencies = Record<string, string[]> | ||
|
||
export type Rels = Record<string, Rel> | ||
export type Rel = { | ||
links: Record<"push" | "replace", string[]> | ||
meta: Metadata | ||
} | ||
|
||
/** | ||
* creates a reference graph | ||
*/ | ||
export const serialize = (graph: Graph) => | ||
Array.from(graph.entries()).map(([decl, refs]) => | ||
[decl.getName(), distinct(refs.map((x) => x.getName()))] as const | ||
) | ||
|
||
type Option<A, B> = { | ||
/** | ||
* Filter and map links, metadatas, and variable declaration | ||
*/ | ||
getLink: (node: VariableDeclaration) => LinkedVarDecl<A, B> | undefined | ||
} | ||
|
||
export const generateGraph = <A, B>({ getLink }: Option<A, B>) => | ||
( | ||
files: SourceFile[], | ||
): { dependencies: Dependencies; relations: Relations<A, B> } => { | ||
const links = files.flatMap((file) => | ||
file.getVariableDeclarations().flatMap((decl) => getLink(decl) ?? []) | ||
) | ||
|
||
const relations = Object.fromEntries( | ||
links.map(({ decl, ...relations }) => [decl.getName(), relations]), | ||
) | ||
const dependencies = Object.fromEntries( | ||
links.flatMap(({ decl }) => serialize(getGraph(decl))), | ||
) | ||
|
||
return { dependencies, relations } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { transpile } from "https://deno.land/x/[email protected]/mod.ts" | ||
import { toFileUrl } from "https://deno.land/[email protected]/path/to_file_url.ts" | ||
|
||
if (import.meta.main) { | ||
const path = toFileUrl(import.meta.dirname + "/main.ts") | ||
|
||
const code = await transpile(path).then((res) => res.get(path.href)!) | ||
console.log(code.length) | ||
await Deno.writeTextFile(import.meta.dirname + "/assets/main.js", code) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { crypto } from "https://deno.land/[email protected]/crypto/mod.ts" | ||
|
||
const toYIQ = ({ r, g, b }: Record<"r" | "g" | "b", number>) => | ||
0.299 * r + 0.587 * g + 0.114 * b | ||
|
||
const encoder = new TextEncoder() | ||
const hashInner = (str: string) => { | ||
const hash = crypto.subtle.digestSync("SHA-1", encoder.encode(str)) | ||
return new Uint8Array(hash) | ||
} | ||
|
||
/** hashes string with SHA-1 */ | ||
export const hash = (str: string): string => { | ||
const hash = hashInner(str) | ||
return Array.from(hash).map((n) => n.toString(16).padStart(2, "0")).join("") | ||
} | ||
|
||
export const hashRGB = (str: string) => { | ||
const [r, g, b] = hashInner(str).slice(0, 3) | ||
return { r, g, b } | ||
} | ||
|
||
export const colors = ({ r, g, b }: Record<"r" | "g" | "b", number>) => | ||
({ | ||
color: `rgba(${r}, ${g}, ${b}, 0.8)`, | ||
textColor: toYIQ({ r, g, b }) > 128 ? "black" : "white", | ||
}) as const |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import type { GraphElement } from "https://deno.land/x/[email protected]/graph/custom/common/link.ts" | ||
import type { GraphData } from "https://esm.sh/[email protected]" | ||
import type { Opaque } from "https://raw.githubusercontent.com/sindresorhus/type-fest/main/source/opaque.d.ts" | ||
|
||
import { ArrowGraphHashed } from "https://deno.land/x/[email protected]/graph/mod.ts" | ||
import { Stream } from "https://deno.land/x/[email protected]/stream/mod.ts" | ||
import { HashSet } from "https://deno.land/x/[email protected]/hashed/mod.ts" | ||
|
||
import { colors, hashRGB } from "./colors.ts" | ||
import { type LabelOption, mkToLabel } from "./label.ts" | ||
|
||
export type Flows = Record<string, { type: "push" | "replace"; to: string }[]> | ||
|
||
export type Id = Opaque<string, Node> | ||
|
||
export type Node = { | ||
id: Id | ||
name: string | ||
textColor: "white" | "black" | ||
color: string | ||
label: string | ||
pathsInto: Id[] | ||
pathsFrom: Id[] | ||
} | ||
export type Link = { | ||
type: "push" | "replace" | ||
source: Id | ||
target: Id | ||
} | ||
|
||
export type Data = { | ||
links: Link[] | ||
nodes: Node[] | ||
} | ||
|
||
export const mkAllPathFrom = | ||
<T>(graph: ArrowGraphHashed<T>) => (id: T): HashSet<T> => { | ||
const visited = HashSet.builder<T>() | ||
const rec = (id: T) => { | ||
const paths = graph.getConnectionStreamFrom(id) | ||
.map(([, to]) => to) | ||
.filter((to) => !visited.has(to)) | ||
.toArray() | ||
|
||
visited.addAll(paths) | ||
paths.forEach(rec) | ||
} | ||
rec(id) | ||
return visited.build() | ||
} | ||
|
||
export const mkAllPathInto = | ||
<T>(graph: ArrowGraphHashed<T>) => (id: T): HashSet<T> => { | ||
const visited = HashSet.builder<T>() | ||
const rec = (id: T) => { | ||
const paths = graph.getConnectionStreamTo(id) | ||
.map(([from]) => from) | ||
.filter((from) => !visited.has(from)) | ||
.toArray() | ||
|
||
visited.addAll(paths) | ||
paths.forEach(rec) | ||
} | ||
rec(id) | ||
return visited.build() | ||
} | ||
|
||
if (import.meta.main) { | ||
const [datapath] = Deno.args | ||
const data = JSON.parse(await Deno.readTextFile(datapath)) as LabelOption | ||
const flows = data.flows | ||
const toLabel = mkToLabel(data) | ||
|
||
const links: Link[] = Object.entries(flows) | ||
.flatMap(([source, targets]) => | ||
targets.map(({ type, to: target }) => ({ | ||
source: source as Id, | ||
target: target as Id, | ||
type, | ||
})) | ||
) | ||
|
||
const graph = Stream.fromObject(flows) | ||
.flatMap(([source, targets]) => | ||
Stream.from(targets).map(({ to }) => [source, to] as GraphElement<Id>) | ||
) | ||
.reduce(ArrowGraphHashed.reducer()) | ||
|
||
const allPathFrom = mkAllPathFrom(graph) | ||
const allPathInto = mkAllPathInto(graph) | ||
|
||
const nodes: Node[] = Object.keys(flows) | ||
.map((name) => { | ||
const id = name as Id | ||
const pathsInto = allPathInto(id).toArray() | ||
const pathsFrom = allPathFrom(id).toArray() | ||
const label = toLabel(name) | ||
const color = colors(hashRGB(name)) | ||
|
||
return { id, name, ...color, pathsInto, pathsFrom, label } | ||
}) | ||
|
||
await Deno.writeTextFile( | ||
import.meta.dirname + "/assets/data.json", | ||
JSON.stringify( | ||
{ links, nodes } satisfies GraphData satisfies { | ||
links: Link[] | ||
nodes: Node[] | ||
}, | ||
null, | ||
2, | ||
), | ||
) | ||
} |
Oops, something went wrong.