@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.
- package/CHANGELOG.md +145 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/bin/astrolabe.mjs +136 -0
- package/package.json +78 -0
- package/src/adapters/nuxt/index.mjs +541 -0
- package/src/adapters/nuxt/project.mjs +174 -0
- package/src/adapters/registry.mjs +23 -0
- package/src/adapters/shared.mjs +191 -0
- package/src/adapters/vue/index.mjs +211 -0
- package/src/adapters/vue/project.mjs +116 -0
- package/src/core/client/app.js +662 -0
- package/src/core/client/overview.js +255 -0
- package/src/core/client/styles.css +669 -0
- package/src/core/diff.mjs +0 -0
- package/src/core/findings.mjs +147 -0
- package/src/core/graph.mjs +148 -0
- package/src/core/layout.mjs +141 -0
- package/src/core/render.mjs +92 -0
- package/src/core/schema.mjs +67 -0
- package/src/core/sfc.mjs +134 -0
- package/src/core/svg.mjs +102 -0
- package/src/core/syntax.mjs +59 -0
- package/src/core/views.mjs +304 -0
- package/src/index.d.ts +155 -0
- package/src/index.mjs +29 -0
|
@@ -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
|
+
}
|