Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dump + dump viewer app #4

Merged
merged 7 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/fastify/bin/start.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { hot } from 'hot-hook'

await hot.init({ root: import.meta.filename })
await hot.init({
root: import.meta.filename,
boundaries: ['../src/services/**.ts'],
})

await import('../src/index.js')
1 change: 1 addition & 0 deletions examples/fastify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@hot-hook/runner": "workspace:*",
"@types/node": "^20.11.17",
"hot-hook": "workspace:*",
"@hot-hook/dump-viewer": "workspace:*",
"pino-pretty": "^11.0.0",
"tsx": "^3.12.2"
},
Expand Down
6 changes: 6 additions & 0 deletions examples/fastify/src/helpers/posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function uppercasePostTitles(posts: any[]) {
return posts.map((post) => ({
...post,
title: post.title.toUpperCase(),
}))
}
13 changes: 12 additions & 1 deletion examples/fastify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,21 @@ const fastify = Fastify({
})

fastify.get('/', async (request, reply) => {
const { PostsService } = await import('./posts_service.js')
const { PostsService } = await import('./services/posts_service.js', { with: { hot: 'true' } })
return new PostsService().getPosts()
})

/**
* This route is totally optional and can be used to visualize
* your dependency graph in a browser.
*/
fastify.get('/dump-viewer', async (request, reply) => {
const { dumpViewer } = await import('@hot-hook/dump-viewer')

reply.header('Content-Type', 'text/html; charset=utf-8')
return dumpViewer()
})

const start = async () => {
try {
await fastify.listen({ port: 3000 })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { uppercasePostTitles } from '../helpers/posts'

export class PostsService {
/**
* Try updating the return value of this method and refreshing the page.
* You will always get the latest version of the code without
* restarting the whole server.
*/
getPosts() {
return [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
{ id: 3, title: 'Post 3' },
]
return uppercasePostTitles([
{ id: 1, title: 'post 1' },
{ id: 2, title: 'post 2' },
{ id: 3, title: 'post 3' },
])
}
}
16 changes: 16 additions & 0 deletions packages/dump_viewer/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hot Hook - Dump Viewer</title>
<script>
window.__HOT_HOOK_DUMP__ = $__hot_hook_placeholder__
</script>
</head>
<body class="h-full m-0">
<div class="h-full" id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
40 changes: 40 additions & 0 deletions packages/dump_viewer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@hot-hook/dump-viewer",
"version": "0.0.0",
"private": true,
"type": "module",
"files": [
"build"
],
"exports": {
".": "./build/dump_viewer.js"
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build && tsc -p tsconfig.node.json",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"lint": "eslint ."
},
"devDependencies": {
"@preact/preset-vite": "^2.8.2",
"hot-hook": "workspace:*",
"typescript": "^5.2.2",
"unocss": "^0.59.0",
"vite": "^5.2.0",
"vite-plugin-singlefile": "^2.0.1"
},
"dependencies": {
"@unocss/reset": "^0.59.0",
"preact": "^10.19.6",
"vis-data": "^7.1.9",
"vis-network": "^9.1.9"
},
"peerDependencies": {
"hot-hook": "workspace:*"
},
"eslintConfig": {
"extends": "@adonisjs/eslint-config/package"
},
"prettier": "@adonisjs/prettier-config"
}
1 change: 1 addition & 0 deletions packages/dump_viewer/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
111 changes: 111 additions & 0 deletions packages/dump_viewer/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { DataSet } from 'vis-data'
import { useEffect } from 'preact/hooks'
import { Edge, Network, Node } from 'vis-network'

export function App() {
async function initGraph() {
const data: HotHookDump = import.meta.env.DEV
? // eslint-disable-next-line unicorn/no-await-expression-member
(await import('./fixtures/dump.json')).default
: window.__HOT_HOOK_DUMP__
let nodes: Node[] = []
let edges: Edge[] = []

const paths = new Map()

data.forEach((item, index) => paths.set(item.path, index + 1))
data.forEach((item, index) => {
const hasOnlyNodeModulesDependency = item.dependencies.every((dep) =>
dep.includes('node_modules')
)

const isRoot = item.dependents.length === 0

const id = index + 1

if (item.path.includes('node_modules') && hasOnlyNodeModulesDependency) {
return
}

let color = 'lightgray'
if (item.boundary) {
color = '#fea800'
} else if (isRoot) {
color = 'violet'
} else if (item.reloadable) {
color = '#72cc0a'
}

nodes.push({
shape: 'box',
id: id,
label: item.path,
margin: { top: 13, bottom: 13, left: 13, right: 13 },
borderWidth: 3,
shadow: true,
font: { color: 'black' },
shapeProperties: { borderRadius: 10 },
color: color,
})

item.dependencies.forEach((dep) => {
const toId = paths.get(dep)
if (toId) {
edges.push({ from: id, to: toId, arrows: 'to' })
} else {
console.log('Missing node for dependency', dep)
}
})
})

const nodesDataSet = new DataSet(nodes)
const edgesDataSet = new DataSet(edges)
const dataVis = { nodes: nodesDataSet, edges: edgesDataSet }

const container = document.getElementById('network')!
new Network(container, dataVis, {
interaction: { hover: true },
layout: {},
physics: {
enabled: true,
barnesHut: {
gravitationalConstant: -8000,
centralGravity: 0.3,
springLength: 95,
springConstant: 0.04,
damping: 0.09,
avoidOverlap: 0,
},
},
})
}

useEffect(() => {
initGraph()
}, [])

return (
<>
<div class="z-1 absolute top-0 left-0 border-2 border-[#353535] px-4 pb-3 pt-2 bg-[#3f3f3f] m-4 rounded-lg text-sm">
<p class="font-bold text-lg">Legend</p>
<div class="flex items-center mt-2">
<div class="w-4 h-4 bg-violet mr-2"></div>
<span>Root entrypoint</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-[#fea800] mr-2"></div>
<span>Boundary file</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-[#72cc0a] mr-2"></div>
<span>Reloadable</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-[#D3D3D3] mr-2"></div>
<span>Not-reloadable</span>
</div>
</div>
<div id="network" class="h-full" />
</>
)
}
22 changes: 22 additions & 0 deletions packages/dump_viewer/src/dump_viewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { join } from 'node:path'
import { readFile } from 'node:fs/promises'

/**
* Returns the HTML content of the Hot Hook Dump Viewer
*/
export async function dumpViewer() {
/**
* Dump the dependency tree
*/
const { hot } = await import('hot-hook')
const dump = await hot.dump()

/**
* Load the HTML content and replace the placeholder with the dump
*/
const htmlLocation = join(import.meta.dirname, 'index.html')
let html = await readFile(htmlLocation, 'utf8')
html = html.replace('$__hot_hook_placeholder__', JSON.stringify(dump))

return html
}
5 changes: 5 additions & 0 deletions packages/dump_viewer/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type HotHookDump = typeof import('./fixtures/dump.json')

interface Window {
__HOT_HOOK_DUMP__: HokHookDump
}
1 change: 1 addition & 0 deletions packages/dump_viewer/src/fixtures/dump.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions packages/dump_viewer/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;

color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;

font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
7 changes: 7 additions & 0 deletions packages/dump_viewer/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { render } from 'preact'
import { App } from './app.tsx'
import '@unocss/reset/tailwind-compat.css'
import 'virtual:uno.css'
import './index.css'

render(<App />, document.getElementById('app')!)
30 changes: 30 additions & 0 deletions packages/dump_viewer/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
},
"types": ["vite/client"],

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
11 changes: 11 additions & 0 deletions packages/dump_viewer/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"skipLibCheck": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./build",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["./src/dump_viewer.ts"]
}
10 changes: 10 additions & 0 deletions packages/dump_viewer/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
import unocss from 'unocss/vite'
import { viteSingleFile } from 'vite-plugin-singlefile'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [preact(), unocss(), viteSingleFile()],
build: { outDir: 'build', emptyOutDir: true },
})
16 changes: 16 additions & 0 deletions packages/hot_hook/src/dependency_tree.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { dirname, relative } from 'node:path'

/**
* Represent a file node in the dependency tree.
*/
Expand Down Expand Up @@ -179,4 +181,18 @@ export default class DependencyTree {
const result = checkPathToRoot(node)
return result
}

dump() {
const rootDirname = dirname(this.#tree.path)
const isNodeModule = (path: string) => path.includes('node_modules')

return Array.from(this.#pathMap.values()).map((node) => ({
path: relative(rootDirname, node.path),
boundary: node.reloadable,
reloadable: isNodeModule(node.path) ? false : this.isReloadable(node.path),
version: node.version,
dependencies: Array.from(node.dependencies).map((n) => relative(rootDirname, n.path)),
dependents: Array.from(node.dependents).map((n) => relative(rootDirname, n.path)),
}))
}
}
Loading
Loading