reslop 0.2.4 → 0.2.6
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 +25 -1
- package/LICENSE +1 -1
- package/README.md +12 -13
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -12
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +48 -18
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +332 -0
- package/lib/dashboard/npm.js +193 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +196 -404
- package/lib/deps.js +240 -399
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +138 -141
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +90 -206
- package/lib/editor.js +98 -55
- package/lib/files.js +86 -103
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +182 -344
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +141 -212
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +157 -132
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +16 -26
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +22 -35
- package/lib/highlight/txt.js +9 -22
- package/lib/highlight.js +17 -46
- package/lib/keys.js +62 -85
- package/lib/manifest.js +67 -130
- package/lib/npm-commands.js +119 -189
- package/lib/remote.js +141 -197
- package/lib/render/branches.js +66 -73
- package/lib/render/chrome.js +264 -323
- package/lib/render/commits.js +187 -500
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +245 -0
- package/lib/render/dash-table.js +290 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -228
- package/lib/render/files.js +45 -99
- package/lib/render/frame.js +129 -183
- package/lib/render/notes.js +144 -160
- package/lib/render/npm.js +234 -112
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +131 -170
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/tiles.js +260 -0
- package/lib/render/unit.js +15 -28
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -38
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -78
- package/lib/report-test.js +256 -141
- package/lib/report-tools.js +96 -161
- package/lib/review-merge.js +114 -152
- package/lib/review.js +115 -189
- package/lib/runs.js +225 -0
- package/lib/select.js +89 -75
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +82 -63
- package/lib/session/branches.js +193 -247
- package/lib/session/change-actions.js +171 -214
- package/lib/session/commit.js +280 -342
- package/lib/session/compose.js +559 -674
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +164 -157
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +85 -53
- package/lib/session/items.js +73 -104
- package/lib/session/lifecycle.js +403 -541
- package/lib/session/load.js +48 -68
- package/lib/session/navigation.js +96 -87
- package/lib/session/npm.js +409 -345
- package/lib/session/ops.js +196 -63
- package/lib/session/pointer.js +23 -15
- package/lib/session/progress.js +33 -21
- package/lib/session/review.js +33 -54
- package/lib/session/tasks.js +333 -0
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -136
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -97
- package/lib/session/view.js +52 -83
- package/lib/session/watch.js +208 -204
- package/lib/session.js +112 -237
- package/lib/update.js +45 -116
- package/lib/utilities.js +67 -89
- package/lib/wrap.js +33 -44
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const tree = require('./tree.js');
|
|
4
|
+
const { folderOf, extOf } = tree;
|
|
5
|
+
const files = require('../files.js');
|
|
6
|
+
const { TASK_KINDS, taskKind } = files;
|
|
7
|
+
|
|
8
|
+
const RUNS_SHOWN = 6;
|
|
9
|
+
const CHAIN_GAP_MS = 2000;
|
|
10
|
+
const NPM_NAMED = /^npm (?:run|exec)(?:\s+-s)?\s+(\S+)/;
|
|
11
|
+
const NPM_ALIAS = /^npm (test|start|stop|restart)(?:\s|$)/;
|
|
12
|
+
const AGE_TEXT = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
|
|
13
|
+
const AGE_SEC = {
|
|
14
|
+
second: 1,
|
|
15
|
+
minute: 60,
|
|
16
|
+
hour: 3600,
|
|
17
|
+
day: 86400,
|
|
18
|
+
week: 604800,
|
|
19
|
+
month: 2592000,
|
|
20
|
+
year: 31536000,
|
|
21
|
+
};
|
|
22
|
+
const STATUS_RANK = { running: 3, failed: 2, aborted: 1, passed: 0 };
|
|
23
|
+
|
|
24
|
+
const namedScript = (command) => {
|
|
25
|
+
const text = `${command ?? ''}`.trim();
|
|
26
|
+
const named = NPM_NAMED.exec(text);
|
|
27
|
+
if (named) return named[1];
|
|
28
|
+
const alias = NPM_ALIAS.exec(text);
|
|
29
|
+
return alias ? alias[1] : '';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const scriptName = (command) => {
|
|
33
|
+
const named = namedScript(command);
|
|
34
|
+
if (named) return named;
|
|
35
|
+
const text = `${command ?? ''}`.trim();
|
|
36
|
+
const token = text.split(/\s+/)[0];
|
|
37
|
+
const slash = Math.max(token.lastIndexOf('/'), token.lastIndexOf('\\'));
|
|
38
|
+
return token.slice(slash + 1) || text;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const runName = (record) => {
|
|
42
|
+
const command = `${record.command ?? ''}`.trim();
|
|
43
|
+
const script = `${record.script ?? ''}`.trim();
|
|
44
|
+
return namedScript(command) || script || command;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ageSeconds = (text) => {
|
|
48
|
+
const raw = `${text ?? ''}`.trim();
|
|
49
|
+
if (/^yesterday$/i.test(raw)) return AGE_SEC.day;
|
|
50
|
+
const match = AGE_TEXT.exec(raw);
|
|
51
|
+
if (!match) return Number.POSITIVE_INFINITY;
|
|
52
|
+
return Number(match[1]) * AGE_SEC[match[2].toLowerCase()];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const newerDate = (current, next) => {
|
|
56
|
+
if (!next) return current;
|
|
57
|
+
if (!current || ageSeconds(next) < ageSeconds(current)) return next;
|
|
58
|
+
return current;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const groupOf = (map, key) => {
|
|
62
|
+
let group = map.get(key);
|
|
63
|
+
if (!group) {
|
|
64
|
+
group = {
|
|
65
|
+
key,
|
|
66
|
+
added: 0,
|
|
67
|
+
removed: 0,
|
|
68
|
+
stagedAdded: 0,
|
|
69
|
+
stagedRemoved: 0,
|
|
70
|
+
staged: 0,
|
|
71
|
+
remaining: 0,
|
|
72
|
+
date: '',
|
|
73
|
+
};
|
|
74
|
+
map.set(key, group);
|
|
75
|
+
}
|
|
76
|
+
return group;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const byChange = (left, right) => {
|
|
80
|
+
const lhs = left.added + left.removed;
|
|
81
|
+
const rhs = right.added + right.removed;
|
|
82
|
+
return rhs - lhs || left.key.localeCompare(right.key, 'en');
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const groupChanges = (entries) => {
|
|
86
|
+
const dirs = new Map();
|
|
87
|
+
const exts = new Map();
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const groups = [
|
|
90
|
+
groupOf(dirs, folderOf(entry.path)),
|
|
91
|
+
groupOf(exts, extOf(entry.path)),
|
|
92
|
+
];
|
|
93
|
+
for (const group of groups) {
|
|
94
|
+
group.added += entry.added ?? 0;
|
|
95
|
+
group.removed += entry.removed ?? 0;
|
|
96
|
+
group.stagedAdded += entry.stagedAdded ?? 0;
|
|
97
|
+
group.stagedRemoved += entry.stagedRemoved ?? 0;
|
|
98
|
+
group.staged += entry.staged ?? 0;
|
|
99
|
+
group.remaining += entry.remaining ?? 0;
|
|
100
|
+
group.date = newerDate(group.date, entry.date);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
dirs: [...dirs.values()].sort(byChange),
|
|
105
|
+
exts: [...exts.values()].sort(byChange),
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const npmRunStatus = (run) => {
|
|
110
|
+
if (run.running) return 'running';
|
|
111
|
+
if (run.exit === 0) return 'passed';
|
|
112
|
+
if (run.exit === '' || run.exit === null) return 'aborted';
|
|
113
|
+
return 'failed';
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const externalRun = (record) => {
|
|
117
|
+
const progress = record.progress ?? {};
|
|
118
|
+
return {
|
|
119
|
+
source: 'reslop t',
|
|
120
|
+
name: runName(record),
|
|
121
|
+
status: record.status,
|
|
122
|
+
exit: record.exit,
|
|
123
|
+
startedAt: record.startedAt,
|
|
124
|
+
endedAt: record.endedAt,
|
|
125
|
+
done: progress.done ?? 0,
|
|
126
|
+
failed: progress.failed ?? 0,
|
|
127
|
+
expected: progress.expected ?? 0,
|
|
128
|
+
result: record.result ?? null,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const internalRun = (run) => ({
|
|
133
|
+
source: 'reslop',
|
|
134
|
+
name: scriptName(run.label),
|
|
135
|
+
status: npmRunStatus(run),
|
|
136
|
+
exit: run.exit,
|
|
137
|
+
startedAt: run.startedAt,
|
|
138
|
+
endedAt: run.endedAt,
|
|
139
|
+
done: 0,
|
|
140
|
+
failed: 0,
|
|
141
|
+
expected: 0,
|
|
142
|
+
result: null,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const rankOf = (status) => STATUS_RANK[status] ?? 0;
|
|
146
|
+
|
|
147
|
+
const isOpen = (run) => run.status === 'running' || !run.endedAt;
|
|
148
|
+
|
|
149
|
+
const addNum = (left, right) => {
|
|
150
|
+
if (typeof left !== 'number') return typeof right === 'number' ? right : null;
|
|
151
|
+
return typeof right === 'number' ? left + right : left;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const mergeResult = (left, right) => {
|
|
155
|
+
if (!left || !right) return left || right;
|
|
156
|
+
const tools = new Set(left.tools);
|
|
157
|
+
for (const tool of right.tools ?? []) tools.add(tool);
|
|
158
|
+
return {
|
|
159
|
+
tools: [...tools],
|
|
160
|
+
tests: addNum(left.tests, right.tests),
|
|
161
|
+
passed: addNum(left.passed, right.passed),
|
|
162
|
+
failed: addNum(left.failed, right.failed),
|
|
163
|
+
skipped: addNum(left.skipped, right.skipped),
|
|
164
|
+
errors: addNum(left.errors, right.errors),
|
|
165
|
+
warnings: addNum(left.warnings, right.warnings),
|
|
166
|
+
duration: left.duration || right.duration || '',
|
|
167
|
+
problems: addNum(left.problems, right.problems),
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const continuesRun = (prev, next) => {
|
|
172
|
+
if (prev.name !== next.name) return false;
|
|
173
|
+
if (isOpen(prev)) return next.startedAt >= prev.startedAt;
|
|
174
|
+
return next.startedAt <= prev.endedAt + CHAIN_GAP_MS;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const absorbRun = (prev, next) => {
|
|
178
|
+
const open = isOpen(prev) || isOpen(next);
|
|
179
|
+
if (rankOf(next.status) >= rankOf(prev.status)) {
|
|
180
|
+
prev.exit = next.exit;
|
|
181
|
+
prev.status = next.status;
|
|
182
|
+
}
|
|
183
|
+
prev.endedAt = open ? 0 : Math.max(prev.endedAt, next.endedAt);
|
|
184
|
+
prev.done += next.done;
|
|
185
|
+
prev.failed += next.failed;
|
|
186
|
+
prev.expected = Math.max(prev.expected, next.expected);
|
|
187
|
+
prev.result = mergeResult(prev.result, next.result);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const collapseRuns = (list) => {
|
|
191
|
+
const ordered = [...list].sort(
|
|
192
|
+
(left, right) => left.startedAt - right.startedAt,
|
|
193
|
+
);
|
|
194
|
+
const groups = [];
|
|
195
|
+
for (const run of ordered) {
|
|
196
|
+
const prev = groups.at(-1);
|
|
197
|
+
if (prev && continuesRun(prev, run)) absorbRun(prev, run);
|
|
198
|
+
else groups.push({ ...run });
|
|
199
|
+
}
|
|
200
|
+
return groups.sort((left, right) => right.startedAt - left.startedAt);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const mergeRuns = (records, npmRun) => {
|
|
204
|
+
const list = records.map(externalRun);
|
|
205
|
+
if (npmRun) list.push(internalRun(npmRun));
|
|
206
|
+
return collapseRuns(list).slice(0, RUNS_SHOWN);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const taskKindCounts = (store) => {
|
|
210
|
+
const kinds = TASK_KINDS.map((kind) => ({
|
|
211
|
+
id: kind.id,
|
|
212
|
+
title: kind.title,
|
|
213
|
+
done: 0,
|
|
214
|
+
total: 0,
|
|
215
|
+
}));
|
|
216
|
+
const byId = new Map(kinds.map((row) => [row.id, row]));
|
|
217
|
+
let done = 0;
|
|
218
|
+
let total = 0;
|
|
219
|
+
if (!store) return { kinds, done, total };
|
|
220
|
+
for (const task of store.tasks) {
|
|
221
|
+
if (!`${task.text ?? ''}`.trim()) continue;
|
|
222
|
+
const row = byId.get(taskKind(task));
|
|
223
|
+
row.total += 1;
|
|
224
|
+
total += 1;
|
|
225
|
+
if (!task.done) continue;
|
|
226
|
+
row.done += 1;
|
|
227
|
+
done += 1;
|
|
228
|
+
}
|
|
229
|
+
return { kinds, done, total };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const versionFields = (pkg, outdatedMap) => {
|
|
233
|
+
const hit = outdatedMap ? outdatedMap.get(pkg.name) : null;
|
|
234
|
+
const current = (hit && hit.current) || pkg.version;
|
|
235
|
+
if (!outdatedMap) return { current, wanted: '', latest: '' };
|
|
236
|
+
if (!hit) return { current, wanted: current, latest: current };
|
|
237
|
+
return { current, wanted: hit.wanted, latest: hit.latest };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const modulesModel = (modules, auditMap, outdatedMap) => {
|
|
241
|
+
const packages = modules.packages.map((pkg) => ({
|
|
242
|
+
name: pkg.name,
|
|
243
|
+
bytes: pkg.bytes,
|
|
244
|
+
dev: pkg.dev,
|
|
245
|
+
audit: Boolean(auditMap && auditMap.get(pkg.name)),
|
|
246
|
+
...versionFields(pkg, outdatedMap),
|
|
247
|
+
}));
|
|
248
|
+
return { ...modules, packages };
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const npmModel = (input) => {
|
|
252
|
+
const info = input.npm;
|
|
253
|
+
const extras = input.npmExtras;
|
|
254
|
+
const auditMap = extras ? extras.auditMap : null;
|
|
255
|
+
const outdatedMap = extras ? extras.outdatedMap : null;
|
|
256
|
+
const npmRun = input.npmRun;
|
|
257
|
+
return {
|
|
258
|
+
ready: Boolean(info),
|
|
259
|
+
hasManifest: info ? info.hasManifest : false,
|
|
260
|
+
deps: info ? info.deps : 0,
|
|
261
|
+
dev: info ? info.dev : 0,
|
|
262
|
+
modules: info ? modulesModel(info.modules, auditMap, outdatedMap) : null,
|
|
263
|
+
audit: auditMap ? auditMap.size : null,
|
|
264
|
+
outdated: outdatedMap ? outdatedMap.size : null,
|
|
265
|
+
running: npmRun && npmRun.running ? npmRun.label : '',
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const filesModel = (input) => {
|
|
270
|
+
const index = input.index;
|
|
271
|
+
const ready = Boolean(index && index.ready);
|
|
272
|
+
const summary = ready ? index.summary() : null;
|
|
273
|
+
const cold = { dirs: new Set(), exts: new Set() };
|
|
274
|
+
return {
|
|
275
|
+
ready,
|
|
276
|
+
total: summary ? summary.total : { files: 0, bytes: 0, lines: 0 },
|
|
277
|
+
dirs: summary ? summary.dirs : [],
|
|
278
|
+
exts: summary ? summary.exts : [],
|
|
279
|
+
hot: index ? index.hot(input.now) : cold,
|
|
280
|
+
delta: input.fileDelta,
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const diffsModel = (input) => {
|
|
285
|
+
const groups = groupChanges(input.entries);
|
|
286
|
+
return {
|
|
287
|
+
totals: input.totals,
|
|
288
|
+
dirs: groups.dirs,
|
|
289
|
+
exts: groups.exts,
|
|
290
|
+
files: input.entries.length,
|
|
291
|
+
samples: input.samples,
|
|
292
|
+
delta: input.diffDelta,
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const commitsModel = (input) => {
|
|
297
|
+
const git = input.git;
|
|
298
|
+
return {
|
|
299
|
+
ready: Boolean(git),
|
|
300
|
+
rebase: git ? git.rebase : null,
|
|
301
|
+
info: git ? git.commits : null,
|
|
302
|
+
changedAt: input.marks.commit,
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const branchesModel = (input) => {
|
|
307
|
+
const git = input.git;
|
|
308
|
+
return {
|
|
309
|
+
ready: Boolean(git),
|
|
310
|
+
list: git ? git.branches : [],
|
|
311
|
+
rebase: git ? git.rebase : null,
|
|
312
|
+
switches: input.switches,
|
|
313
|
+
hot: input.branchAt,
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const tasksModel = (input) => taskKindCounts(input.store);
|
|
318
|
+
|
|
319
|
+
const buildModel = (input) => ({
|
|
320
|
+
now: input.now,
|
|
321
|
+
frame: input.frame,
|
|
322
|
+
busy: input.busy,
|
|
323
|
+
files: filesModel(input),
|
|
324
|
+
diffs: diffsModel(input),
|
|
325
|
+
commits: commitsModel(input),
|
|
326
|
+
branches: branchesModel(input),
|
|
327
|
+
npm: npmModel(input),
|
|
328
|
+
runs: mergeRuns(input.runs, input.npmRun),
|
|
329
|
+
tasks: tasksModel(input),
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
module.exports = { groupChanges, mergeRuns, scriptName, runName, buildModel };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const deps = require('../deps.js');
|
|
7
|
+
const { MANIFEST } = deps;
|
|
8
|
+
const watch = require('../session/watch.js');
|
|
9
|
+
const { MODULES_STAMP } = watch;
|
|
10
|
+
|
|
11
|
+
const MODULES = 'node_modules';
|
|
12
|
+
const MODULE_PREFIX = `${MODULES}/`;
|
|
13
|
+
const WORKERS = 8;
|
|
14
|
+
const TOP_PACKAGES = 8;
|
|
15
|
+
|
|
16
|
+
const readJson = async (file) => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(await fs.promises.readFile(file, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const declaredNames = (pkg, section) => {
|
|
25
|
+
const map = pkg && pkg[section];
|
|
26
|
+
if (!map || typeof map !== 'object') return [];
|
|
27
|
+
return Object.keys(map);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const lockPackages = (lock) => {
|
|
31
|
+
const packages = lock && lock.packages;
|
|
32
|
+
return packages && typeof packages === 'object' ? packages : null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const modulesStamp = async (top) => {
|
|
36
|
+
try {
|
|
37
|
+
const stat = await fs.promises.stat(path.join(top, MODULES_STAMP));
|
|
38
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
39
|
+
} catch {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const packageName = (root, file) => {
|
|
45
|
+
const parts = path.relative(root, file).split(path.sep);
|
|
46
|
+
const head = parts[0];
|
|
47
|
+
if (!head || head.startsWith('.')) return '';
|
|
48
|
+
if (!head.startsWith('@')) return head;
|
|
49
|
+
const name = parts[1];
|
|
50
|
+
if (!name || name.startsWith('.')) return '';
|
|
51
|
+
return `${head}/${name}`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const rankedPackages = (totals) => {
|
|
55
|
+
const list = [];
|
|
56
|
+
for (const [name, bytes] of totals) list.push({ name, bytes });
|
|
57
|
+
return list.sort(
|
|
58
|
+
(left, right) =>
|
|
59
|
+
right.bytes - left.bytes || left.name.localeCompare(right.name, 'en'),
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const devOnlyNames = (pkg, lock) => {
|
|
64
|
+
const keep = new Set([
|
|
65
|
+
...declaredNames(pkg, 'dependencies'),
|
|
66
|
+
...declaredNames(pkg, 'optionalDependencies'),
|
|
67
|
+
]);
|
|
68
|
+
const names = new Set();
|
|
69
|
+
for (const name of declaredNames(pkg, 'devDependencies')) {
|
|
70
|
+
if (!keep.has(name)) names.add(name);
|
|
71
|
+
}
|
|
72
|
+
const packages = lockPackages(lock);
|
|
73
|
+
if (!packages) return names;
|
|
74
|
+
for (const key of Object.keys(packages)) {
|
|
75
|
+
const entry = packages[key];
|
|
76
|
+
if (!entry || !entry.dev || !key.startsWith(MODULE_PREFIX)) continue;
|
|
77
|
+
const name = key.slice(MODULE_PREFIX.length);
|
|
78
|
+
if (!name || name.includes('/node_modules/') || keep.has(name)) continue;
|
|
79
|
+
names.add(name);
|
|
80
|
+
}
|
|
81
|
+
return names;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const lockVersion = (lock, name) => {
|
|
85
|
+
const packages = lockPackages(lock);
|
|
86
|
+
const entry = packages && packages[`${MODULE_PREFIX}${name}`];
|
|
87
|
+
if (!entry || typeof entry.version !== 'string') return '';
|
|
88
|
+
return entry.version;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const topPackages = (ranked, pkg, lock) => {
|
|
92
|
+
const devNames = devOnlyNames(pkg, lock);
|
|
93
|
+
const rows = ranked.map((item) => ({
|
|
94
|
+
name: item.name,
|
|
95
|
+
bytes: item.bytes,
|
|
96
|
+
dev: devNames.has(item.name),
|
|
97
|
+
version: lockVersion(lock, item.name),
|
|
98
|
+
}));
|
|
99
|
+
const runtime = rows.filter((row) => !row.dev);
|
|
100
|
+
const devs = rows.filter((row) => row.dev);
|
|
101
|
+
return [...runtime, ...devs].slice(0, TOP_PACKAGES);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const measureModules = async (root) => {
|
|
105
|
+
let total = 0;
|
|
106
|
+
const totals = new Map();
|
|
107
|
+
const queue = [root];
|
|
108
|
+
const worker = async () => {
|
|
109
|
+
while (queue.length) {
|
|
110
|
+
const dir = queue.pop();
|
|
111
|
+
let entries;
|
|
112
|
+
try {
|
|
113
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const files = [];
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const full = path.join(dir, entry.name);
|
|
120
|
+
if (entry.isDirectory()) queue.push(full);
|
|
121
|
+
else if (entry.isFile()) files.push(full);
|
|
122
|
+
}
|
|
123
|
+
const sizes = await Promise.all(
|
|
124
|
+
files.map((file) =>
|
|
125
|
+
fs.promises.lstat(file).then(
|
|
126
|
+
(stat) => ({ name: packageName(root, file), bytes: stat.size }),
|
|
127
|
+
() => ({ name: '', bytes: 0 }),
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
for (const item of sizes) {
|
|
132
|
+
total += item.bytes;
|
|
133
|
+
if (!item.name || !item.bytes) continue;
|
|
134
|
+
totals.set(item.name, (totals.get(item.name) ?? 0) + item.bytes);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
await Promise.all(Array.from({ length: WORKERS }, worker));
|
|
139
|
+
return { bytes: total, ranked: rankedPackages(totals) };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const installedCount = async (top, lock) => {
|
|
143
|
+
const packages = lockPackages(lock);
|
|
144
|
+
if (packages) {
|
|
145
|
+
let count = 0;
|
|
146
|
+
for (const key of Object.keys(packages)) {
|
|
147
|
+
if (key && !packages[key].link) count += 1;
|
|
148
|
+
}
|
|
149
|
+
return count;
|
|
150
|
+
}
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = await fs.promises.readdir(path.join(top, MODULES));
|
|
154
|
+
} catch {
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
let count = 0;
|
|
158
|
+
for (const name of entries) {
|
|
159
|
+
if (name.startsWith('.')) continue;
|
|
160
|
+
if (!name.startsWith('@')) {
|
|
161
|
+
count += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const scoped = await fs.promises.readdir(path.join(top, MODULES, name));
|
|
165
|
+
count += scoped.length;
|
|
166
|
+
}
|
|
167
|
+
return count;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const readModules = async (top, lock, previous) => {
|
|
171
|
+
const stamp = await modulesStamp(top);
|
|
172
|
+
if (previous && stamp && previous.stamp === stamp) return previous;
|
|
173
|
+
const [count, measured] = await Promise.all([
|
|
174
|
+
installedCount(top, lock),
|
|
175
|
+
measureModules(path.join(top, MODULES)),
|
|
176
|
+
]);
|
|
177
|
+
return { stamp, count, bytes: measured.bytes, ranked: measured.ranked };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const readNpmSummary = async (top, previous) => {
|
|
181
|
+
const pkg = await readJson(path.join(top, MANIFEST));
|
|
182
|
+
const lock = await readJson(path.join(top, MODULES_STAMP));
|
|
183
|
+
const cached = previous ? previous.modules : null;
|
|
184
|
+
const modules = await readModules(top, lock, cached);
|
|
185
|
+
return {
|
|
186
|
+
hasManifest: Boolean(pkg),
|
|
187
|
+
deps: declaredNames(pkg, 'dependencies').length,
|
|
188
|
+
dev: declaredNames(pkg, 'devDependencies').length,
|
|
189
|
+
modules: { ...modules, packages: topPackages(modules.ranked, pkg, lock) },
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
module.exports = { readNpmSummary };
|