@amulet-laboratories/astrolabe 0.4.0

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.
@@ -0,0 +1,255 @@
1
+ /**
2
+ * The overview is what the panel shows when nothing is selected — the repo
3
+ * summary you'd want before touching a line of it: what it's built on, how big
4
+ * it is, what's notable, and what's wrong. Previously this space said "select a
5
+ * node", which is a prompt, not information.
6
+ */
7
+
8
+ function renderOverview(body) {
9
+ body.className = 'overview'
10
+ body.replaceChildren()
11
+
12
+ const h = document.createElement('h2')
13
+ h.textContent = G.project.name
14
+ body.append(h)
15
+ if (G.project.description) {
16
+ const d = document.createElement('div')
17
+ d.className = 'kind'
18
+ d.textContent = G.project.description
19
+ body.append(d)
20
+ }
21
+
22
+ body.append(statBand())
23
+
24
+ if (G.findings.length) body.append(section('Findings', [findingsList()]))
25
+ body.append(section('Composition', [compositionBar(), compositionList()]))
26
+
27
+ const hot = mostReferenced(6)
28
+ if (hot.length) body.append(section('Most referenced', [refTable(hot, (n) => `${n.count} refs`)]))
29
+
30
+ const heavy = heaviestFiles(6)
31
+ if (heavy.length)
32
+ body.append(section('Largest files', [refTable(heavy, (n) => `${n.count} lines`)]))
33
+
34
+ body.append(section('Stack', [stackList()]))
35
+ body.append(section('How to read this', [legend()]))
36
+ }
37
+
38
+ function statBand() {
39
+ const stats = [
40
+ [count('route'), 'routes'],
41
+ [count('component'), 'components'],
42
+ [G.stats.loc.toLocaleString(), 'lines'],
43
+ [
44
+ G.stats.contentFiles || count('collection'),
45
+ G.stats.contentFiles ? 'content files' : 'collections',
46
+ ],
47
+ ]
48
+ const d = document.createElement('div')
49
+ d.className = 'stat-band'
50
+ for (const [value, label] of stats) {
51
+ const cell = document.createElement('div')
52
+ const v = document.createElement('strong')
53
+ v.textContent = value
54
+ const l = document.createElement('span')
55
+ l.textContent = label
56
+ cell.append(v, l)
57
+ d.append(cell)
58
+ }
59
+ return d
60
+ }
61
+
62
+ const count = (type) => G.nodes.filter((n) => n.type === type).length
63
+
64
+ /** One bar showing the shape of the codebase at a glance. */
65
+ function compositionBar() {
66
+ const bar = document.createElement('div')
67
+ bar.className = 'bar'
68
+ const total = G.nodes.length
69
+ for (const [type, n] of typeCounts()) {
70
+ const seg = document.createElement('span')
71
+ seg.style.width = `${(n / total) * 100}%`
72
+ seg.style.background = `var(--${TYPES[type].color})`
73
+ seg.title = `${n} ${TYPES[type].label}`
74
+ bar.append(seg)
75
+ }
76
+ return bar
77
+ }
78
+
79
+ function compositionList() {
80
+ const ul = document.createElement('ul')
81
+ ul.className = 'legend-list'
82
+ for (const [type, n] of typeCounts()) {
83
+ const li = document.createElement('li')
84
+ const dot = document.createElement('span')
85
+ dot.className = 'dot'
86
+ dot.style.background = `var(--${TYPES[type].color})`
87
+ const name = document.createElement('span')
88
+ name.textContent = TYPES[type].label
89
+ const num = document.createElement('span')
90
+ num.className = 'rel'
91
+ num.textContent = n
92
+ li.append(dot, name, num)
93
+ ul.append(li)
94
+ }
95
+ return ul
96
+ }
97
+
98
+ function typeCounts() {
99
+ const counts = new Map()
100
+ for (const n of G.nodes) counts.set(n.type, (counts.get(n.type) ?? 0) + 1)
101
+ return [...counts].sort((a, b) => b[1] - a[1])
102
+ }
103
+
104
+ function findingsList() {
105
+ const ul = document.createElement('ul')
106
+ ul.className = 'findings'
107
+ for (const f of G.findings) {
108
+ const li = document.createElement('li')
109
+ const head = document.createElement('div')
110
+ head.className = 'finding-head'
111
+ const dot = document.createElement('span')
112
+ dot.className = `sev sev-${f.severity}`
113
+ const title = document.createElement('span')
114
+ title.textContent = f.title
115
+ head.append(dot, title)
116
+
117
+ const detail = document.createElement('div')
118
+ detail.className = 'finding-detail'
119
+ detail.textContent = f.detail
120
+
121
+ li.append(head, detail)
122
+
123
+ // The names are the point — a count you can't act on is just a number.
124
+ const names = document.createElement('div')
125
+ names.className = 'tags'
126
+ for (const id of f.nodes.slice(0, 12)) {
127
+ const node = RAW_BY_ID.get(id)
128
+ if (!node) continue
129
+ const b = document.createElement('button')
130
+ b.className = 'tag tag-btn'
131
+ b.type = 'button'
132
+ b.textContent = node.label
133
+ b.addEventListener('click', () => selectNode(id))
134
+ names.append(b)
135
+ }
136
+ if (f.nodes.length > 12) {
137
+ const more = document.createElement('span')
138
+ more.className = 'tag'
139
+ more.textContent = `+${f.nodes.length - 12} more`
140
+ names.append(more)
141
+ }
142
+ li.append(names)
143
+ ul.append(li)
144
+ }
145
+ return ul
146
+ }
147
+
148
+ function mostReferenced(limit) {
149
+ const counts = new Map()
150
+ for (const e of G.edges) counts.set(e.to, (counts.get(e.to) ?? 0) + 1)
151
+ return [...counts]
152
+ .map(([id, n]) => ({ node: RAW_BY_ID.get(id), count: n }))
153
+ .filter((r) => r.node)
154
+ .sort((a, b) => b.count - a.count)
155
+ .slice(0, limit)
156
+ }
157
+
158
+ function heaviestFiles(limit) {
159
+ return G.nodes
160
+ .filter((n) => n.meta.loc)
161
+ .map((n) => ({ node: n, count: n.meta.loc }))
162
+ .sort((a, b) => b.count - a.count)
163
+ .slice(0, limit)
164
+ }
165
+
166
+ function refTable(rows, format) {
167
+ const ul = document.createElement('ul')
168
+ ul.className = 'rank'
169
+ for (const row of rows) {
170
+ const li = document.createElement('li')
171
+ const b = document.createElement('button')
172
+ b.className = 'ref'
173
+ b.type = 'button'
174
+ b.textContent = row.node.label
175
+ b.style.color = colorOf(row.node)
176
+ b.addEventListener('click', () => selectNode(row.node.id))
177
+ const n = document.createElement('span')
178
+ n.className = 'rel'
179
+ n.textContent = format(row)
180
+ li.append(b, n)
181
+ ul.append(li)
182
+ }
183
+ return ul
184
+ }
185
+
186
+ function stackList() {
187
+ const frag = document.createDocumentFragment()
188
+ const facts = []
189
+ if (G.project.stack?.nuxt) facts.push(['Nuxt', G.project.stack.nuxt])
190
+ if (G.project.stack?.node) facts.push(['Node', G.project.stack.node])
191
+ if (G.project.stack?.packageManager)
192
+ facts.push(['Package manager', G.project.stack.packageManager])
193
+ if (facts.length) {
194
+ const ul = document.createElement('ul')
195
+ for (const [k, v] of facts) {
196
+ const li = document.createElement('li')
197
+ const label = document.createElement('span')
198
+ label.className = 'rel'
199
+ label.textContent = `${k} `
200
+ li.append(label, document.createTextNode(v))
201
+ ul.append(li)
202
+ }
203
+ frag.append(ul)
204
+ }
205
+ if (G.project.modules?.length) {
206
+ const h = document.createElement('h3')
207
+ h.textContent = 'Nuxt modules'
208
+ frag.append(h, tagList(G.project.modules))
209
+ }
210
+ const deps = G.project.stack?.dependencies ?? []
211
+ if (deps.length) {
212
+ const h = document.createElement('h3')
213
+ h.textContent = `Dependencies (${deps.filter((d) => !d.dev).length} runtime, ${deps.filter((d) => d.dev).length} dev)`
214
+ const d = document.createElement('div')
215
+ d.className = 'tags'
216
+ for (const dep of deps) {
217
+ const s = document.createElement('span')
218
+ s.className = `tag${dep.dev ? ' tag-dim' : ''}`
219
+ s.textContent = dep.name
220
+ s.title = `${dep.name}@${dep.version}${dep.dev ? ' (dev)' : ''}`
221
+ d.append(s)
222
+ }
223
+ frag.append(h, d)
224
+ }
225
+ return frag
226
+ }
227
+
228
+ function legend() {
229
+ const ul = document.createElement('ul')
230
+ ul.className = 'legend-list'
231
+ for (const [kind, verb] of Object.entries(EDGE_VERB)) {
232
+ if (kind === 'depends') continue
233
+ const li = document.createElement('li')
234
+ const line = document.createElement('span')
235
+ line.className = 'edge-swatch'
236
+ const name = document.createElement('span')
237
+ name.textContent = verb
238
+ const hint = document.createElement('span')
239
+ hint.className = 'rel'
240
+ hint.textContent = EDGE_HINT[kind] ?? ''
241
+ li.append(line, name, hint)
242
+ ul.append(li)
243
+ }
244
+ return ul
245
+ }
246
+
247
+ const EDGE_HINT = {
248
+ renders: 'template or markdown',
249
+ uses: 'composable call',
250
+ queries: 'queryCollection()',
251
+ fetches: '$fetch / useFetch',
252
+ reads: 'app or runtime config',
253
+ wraps: 'layout',
254
+ guards: 'middleware',
255
+ }