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.
Files changed (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +196 -404
  14. package/lib/deps.js +240 -399
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +138 -141
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +157 -132
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +16 -26
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. 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 };