reslop 0.1.9 → 0.2.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +2 -1
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +4 -2
  5. package/lib/api.js +2 -2
  6. package/lib/capabilities.js +7 -14
  7. package/lib/cli.js +28 -47
  8. package/lib/clipboard.js +1 -1
  9. package/lib/{deps/changes.js → deps-diff.js} +309 -19
  10. package/lib/deps.js +498 -29
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +5 -8
  20. package/lib/{git/branches.js → git-branches.js} +6 -5
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git.js +589 -68
  23. package/lib/github.js +28 -197
  24. package/lib/gitlab.js +24 -195
  25. package/lib/highlight/bash.js +1 -3
  26. package/lib/highlight/css.js +1 -3
  27. package/lib/highlight/js.js +11 -14
  28. package/lib/highlight.js +6 -9
  29. package/lib/keys.js +14 -8
  30. package/lib/manifest.js +330 -0
  31. package/lib/remote.js +374 -0
  32. package/lib/render/branches.js +14 -68
  33. package/lib/render/chrome.js +24 -9
  34. package/lib/render/commits.js +6 -59
  35. package/lib/render/diff.js +16 -16
  36. package/lib/render/files.js +6 -34
  37. package/lib/render/frame.js +23 -12
  38. package/lib/render/notes.js +4 -3
  39. package/lib/render/primitives.js +45 -0
  40. package/lib/{render.js → render/render.js} +5 -5
  41. package/lib/render/unit.js +41 -0
  42. package/lib/review.js +750 -14
  43. package/lib/select.js +5 -10
  44. package/lib/session/accessors.js +9 -1
  45. package/lib/session/actions.js +32 -16
  46. package/lib/session/branches.js +116 -128
  47. package/lib/session/change-actions.js +202 -123
  48. package/lib/session/commit.js +105 -112
  49. package/lib/session/compose.js +158 -13
  50. package/lib/session/dispatch.js +104 -60
  51. package/lib/session/files-pane.js +142 -148
  52. package/lib/session/items.js +22 -30
  53. package/lib/session/lifecycle.js +44 -44
  54. package/lib/session/load.js +19 -33
  55. package/lib/session/navigation.js +12 -1
  56. package/lib/session/ops.js +28 -41
  57. package/lib/session/pointer.js +54 -57
  58. package/lib/session/review.js +23 -37
  59. package/lib/session/terminal.js +6 -12
  60. package/lib/session/todos.js +8 -12
  61. package/lib/session/unit.js +208 -0
  62. package/lib/session/update.js +33 -46
  63. package/lib/session/view.js +78 -9
  64. package/lib/session/watch.js +11 -19
  65. package/lib/session.js +21 -246
  66. package/lib/source.js +3 -3
  67. package/lib/update.js +40 -75
  68. package/lib/{git/proc.js → utilities.js} +75 -11
  69. package/lib/wrap.js +1 -3
  70. package/package.json +1 -1
  71. package/lib/deps/items.js +0 -134
  72. package/lib/deps/manifest.js +0 -152
  73. package/lib/deps/merge.js +0 -120
  74. package/lib/deps/proposals.js +0 -164
  75. package/lib/deps/reports.js +0 -180
  76. package/lib/deps/rows.js +0 -176
  77. package/lib/deps/scan.js +0 -138
  78. package/lib/deps/usage.js +0 -68
  79. package/lib/deps/versions.js +0 -119
  80. package/lib/diff.js +0 -33
  81. package/lib/git/changes.js +0 -164
  82. package/lib/git/read.js +0 -253
  83. package/lib/git/run.js +0 -100
  84. package/lib/npm/commands.js +0 -100
  85. package/lib/remote/changes.js +0 -37
  86. package/lib/remote/headers.js +0 -36
  87. package/lib/remote/http.js +0 -122
  88. package/lib/remote/notes.js +0 -94
  89. package/lib/review/format.js +0 -434
  90. package/lib/review/storage.js +0 -150
  91. package/lib/review/store.js +0 -178
  92. package/lib/review/templates.js +0 -49
  93. package/lib/session/origins.js +0 -7
  94. package/lib/sys.js +0 -84
@@ -74,12 +74,13 @@ const closeWatcher = (watcher) => {
74
74
 
75
75
  const schedule = (state) => {
76
76
  if (state.closed) return;
77
- if (state.timer !== null) state.clearTimeoutFn(state.timer);
78
- state.timer = state.setTimeoutFn(() => {
77
+ if (state.timer !== null) clearTimeout(state.timer);
78
+ state.timer = setTimeout(() => {
79
79
  state.timer = null;
80
80
  if (state.closed) return;
81
81
  state.onChange();
82
82
  }, state.debounceMs);
83
+ if (typeof state.timer.unref === 'function') state.timer.unref();
83
84
  };
84
85
 
85
86
  const handleWatchEvent = (state, dir, eventType, filename) => {
@@ -100,9 +101,9 @@ const watchOne = (state, target, extra = {}) => {
100
101
  const onEvent = (eventType, filename) => {
101
102
  handleWatchEvent(state, target, eventType, filename);
102
103
  };
103
- const watcher = state.watchFn(
104
+ const watcher = fs.watch(
104
105
  target,
105
- { persistent: true, encoding: 'utf8', ...extra },
106
+ { persistent: false, encoding: 'utf8', ...extra },
106
107
  onEvent,
107
108
  );
108
109
  if (typeof watcher.on === 'function') {
@@ -120,7 +121,7 @@ const watchTree = (state, dir) => {
120
121
  if (state.recursive) return;
121
122
  let entries;
122
123
  try {
123
- entries = state.readdirSync(dir, { withFileTypes: true });
124
+ entries = fs.readdirSync(dir, { withFileTypes: true });
124
125
  } catch {
125
126
  return;
126
127
  }
@@ -135,7 +136,7 @@ const maybeWatchNewDir = (state, full) => {
135
136
  if (state.recursive) return;
136
137
  let stat;
137
138
  try {
138
- stat = state.statSync(full);
139
+ stat = fs.statSync(full);
139
140
  } catch {
140
141
  return;
141
142
  }
@@ -148,15 +149,11 @@ const watchScope = (state, spec) => {
148
149
  const target = path.resolve(state.root, spec);
149
150
  let stat;
150
151
  try {
151
- stat = state.statSync(target);
152
+ stat = fs.statSync(target);
152
153
  } catch {
153
- watchOne(state, path.dirname(target));
154
- return;
155
- }
156
- if (stat.isDirectory()) {
157
- watchTree(state, target);
158
- return;
154
+ return void watchOne(state, path.dirname(target));
159
155
  }
156
+ if (stat.isDirectory()) return void watchTree(state, target);
160
157
  watchOne(state, path.dirname(target));
161
158
  };
162
159
 
@@ -168,7 +165,7 @@ const watchGit = (state) => {
168
165
 
169
166
  const stopState = (state) => {
170
167
  state.closed = true;
171
- if (state.timer !== null) state.clearTimeoutFn(state.timer);
168
+ if (state.timer !== null) clearTimeout(state.timer);
172
169
  state.timer = null;
173
170
  while (state.watchers.length) closeWatcher(state.watchers.pop());
174
171
  state.watched.clear();
@@ -186,11 +183,6 @@ const createDiskWatcher = (options) => {
186
183
  const recursive = options.recursive ?? canWatchRecursive();
187
184
  const state = {
188
185
  root: path.resolve(options.root),
189
- watchFn: options.watch ?? fs.watch,
190
- readdirSync: options.readdirSync ?? fs.readdirSync,
191
- statSync: options.statSync ?? fs.statSync,
192
- setTimeoutFn: options.setTimeout ?? setTimeout,
193
- clearTimeoutFn: options.clearTimeout ?? clearTimeout,
194
186
  debounceMs: options.debounceMs ?? DEBOUNCE_MS,
195
187
  onChange: options.onChange,
196
188
  recursive,
package/lib/session.js CHANGED
@@ -1,16 +1,15 @@
1
1
  'use strict';
2
2
 
3
- const render = require('./render.js');
3
+ const render = require('./render/render.js');
4
4
  const keys = require('./keys.js');
5
5
  const review = require('./review.js');
6
- const { copyText } = require('./clipboard.js');
7
6
  const files = require('./files.js');
8
7
  const { makeTodoItem } = files;
9
8
  const { renderFrame } = render;
10
9
  const { decodeChunk } = keys;
11
10
  const { AUTOSAVE_MS, hasNotes } = review;
12
11
  const capabilities = require('./capabilities.js');
13
- const { sessionCapabilities, createReviewStorage } = capabilities;
12
+ const { sessionCapabilities } = capabilities;
14
13
  const { createTerminal, LEAVE_TERM } = require('./session/terminal.js');
15
14
  const { createLoadCoordinator } = require('./session/load.js');
16
15
  const items = require('./session/items.js');
@@ -35,13 +34,6 @@ const { createDispatcher, createInput } = require('./session/dispatch.js');
35
34
  const CURSOR_BLINK_MS = 500;
36
35
  const PROGRESS_MS = 80;
37
36
 
38
- const systemNow = () => new Date();
39
-
40
- const targetHasStaged = (session) => {
41
- if (session.repo.hasStaged) return session.repo.hasStaged(session.top);
42
- return session.items.some((item) => item.origin === 'staged');
43
- };
44
-
45
37
  const NAV_FIELDS = [
46
38
  'pane',
47
39
  'index',
@@ -51,6 +43,8 @@ const NAV_FIELDS = [
51
43
  'scroll',
52
44
  'listScroll',
53
45
  'reviewPath',
46
+ 'fileScope',
47
+ 'unitLine',
54
48
  'selection',
55
49
  'mouseAnchor',
56
50
  'pendingClick',
@@ -77,51 +71,34 @@ const assignOptions = (session, options) => {
77
71
  session.stdout = options.stdout;
78
72
  session.proc = options.proc ?? process;
79
73
  session.color = options.color ?? true;
80
- session.copyText = options.copyText ?? copyText;
81
- const termSize = () => {
74
+ session.getSize = () => {
82
75
  const width = session.stdout.columns ?? 80;
83
76
  const height = session.stdout.rows ?? 24;
84
77
  return { width, height };
85
78
  };
86
- session.getSize = options.getSize ?? termSize;
87
79
  session.rev = options.rev ?? null;
88
80
  session.revShort = '';
89
81
  session.sourceLabel = options.sourceLabel ?? '';
90
82
  session.repoName = options.repoName ?? '';
91
83
  session.change = options.change ?? null;
92
- session.now = options.now ?? systemNow;
93
84
  session.forceNewReview = options.newReview === true;
94
85
  session.readOnly = options.readOnly === true;
95
86
  session.capabilities = sessionCapabilities(options);
96
- const storage = options.reviewStorage ?? createReviewStorage(options);
97
87
  session.audit = options.audit === true;
98
88
  session.outdatedMap = options.outdatedMap;
99
89
  session.auditMap = options.auditMap;
100
- session.setInterval = options.setInterval ?? setInterval;
101
- session.clearInterval = options.clearInterval ?? clearInterval;
102
- session.watchOpts = {
103
- watch: options.watch,
104
- readdirSync: options.watchReaddir,
105
- statSync: options.watchStat,
106
- setTimeout: options.setTimeout ?? setTimeout,
107
- clearTimeout: options.clearTimeout ?? clearTimeout,
108
- debounceMs: options.watchDebounceMs,
109
- recursive: options.watchRecursive,
110
- };
111
90
  session.updateOpts = options.update;
112
91
  session.term = createTerminal({
113
92
  stdin: session.stdin,
114
93
  stdout: session.stdout,
115
94
  proc: session.proc,
116
- setInterval: session.setInterval,
117
- clearInterval: session.clearInterval,
118
95
  });
119
96
  session.loader = createLoadCoordinator();
120
97
  session.collection = createItemCollection();
121
98
  session.nav = createNavigation({
122
99
  startPane: options.startPane ?? 'files',
123
100
  });
124
- session.review = createReviewController({ storage });
101
+ session.review = createReviewController();
125
102
  session.progress = createProgress(
126
103
  session.term,
127
104
  () => session.tickProgress(),
@@ -129,28 +106,6 @@ const assignOptions = (session, options) => {
129
106
  );
130
107
  };
131
108
 
132
- const assignUpdater = (session) => {
133
- session.updater = createUpdateCoordinator({
134
- updateOpts: () => session.updateOpts,
135
- didLoad: () => session.loader.didLoad,
136
- getMode: () => session.mode,
137
- setMode: (mode) => {
138
- session.mode = mode;
139
- },
140
- isDone: () => session.done,
141
- uiOpen: () => session.uiOpen,
142
- paint: () => session.paint(),
143
- setStatus: (value) => {
144
- session.status = value;
145
- },
146
- startInstallProgress: () => {
147
- session.progressFrame = 0;
148
- session.progress.start('install');
149
- },
150
- stopInstallProgress: () => session.progress.stop('install'),
151
- });
152
- };
153
-
154
109
  const assignComposer = (session) => {
155
110
  session.composer = createComposer({
156
111
  nav: session.nav,
@@ -168,77 +123,10 @@ const assignComposer = (session) => {
168
123
  };
169
124
 
170
125
  const assignControllers = (session) => {
171
- const setStatus = (value) => {
172
- session.status = value;
173
- };
174
- const setMode = (mode) => {
175
- session.mode = mode;
176
- };
177
- session.ops = createOpsRunner({
178
- top: () => session.top,
179
- isDone: () => session.done,
180
- getMode: () => session.mode,
181
- setMode,
182
- getStatus: () => session.status,
183
- setStatus,
184
- paint: () => session.paint(),
185
- startProgress: () => session.startProgress(),
186
- stopProgress: () => session.stopProgress(),
187
- resetProgressFrame: () => {
188
- session.progressFrame = 0;
189
- },
190
- });
191
- session.changeActions = createChangeActions({
192
- repo: () => session.repo,
193
- top: () => session.top,
194
- capabilities: () => session.capabilities,
195
- uiOpen: () => session.uiOpen,
196
- collection: session.collection,
197
- getItems: () => session.items,
198
- currentItem: () => session.current(),
199
- nav: session.nav,
200
- fileList: () => session.fileList(),
201
- runBusy: session.ops.runBusy,
202
- reloadAfterChange: (afterLoad) => session.reloadAfterChange(afterLoad),
203
- setStatus,
204
- syncReviewPath: () => session.syncReviewPath(),
205
- syncFileCursor: () => session.syncFileCursor(),
206
- ignoreWatch: () => session.lifecycle.ignoreWatch(),
207
- });
208
- session.commits = createCommitController({
209
- repo: () => session.repo,
210
- top: () => session.top,
211
- capabilities: () => session.capabilities,
212
- getItems: () => session.items,
213
- uiOpen: () => session.uiOpen,
214
- composer: session.composer,
215
- collection: session.collection,
216
- runner: session.ops,
217
- refreshFromRepo: (opts) => session.refreshFromRepo(opts),
218
- setMode,
219
- setStatus,
220
- isDone: () => session.done,
221
- pendingExtras: () => session.pendingExtras,
222
- getMode: () => session.mode,
223
- getStatus: () => session.status,
224
- nav: session.nav,
225
- });
226
- session.gitBranches = createBranchController({
227
- repo: () => session.repo,
228
- top: () => session.top,
229
- capabilities: () => session.capabilities,
230
- uiOpen: () => session.uiOpen,
231
- composer: session.composer,
232
- nav: session.nav,
233
- runner: session.ops,
234
- canCommit: session.commits.canCommit,
235
- refreshFromRepo: (opts) => session.refreshFromRepo(opts),
236
- showFiles: () => session.showFiles(),
237
- getMode: () => session.mode,
238
- setMode,
239
- getStatus: () => session.status,
240
- setStatus,
241
- });
126
+ session.ops = createOpsRunner(session);
127
+ session.changeActions = createChangeActions(session);
128
+ session.commits = createCommitController(session);
129
+ session.gitBranches = createBranchController(session);
242
130
  };
243
131
 
244
132
  const bindSessionAccessors = (session) => {
@@ -302,29 +190,6 @@ const bindViewer = (session) => {
302
190
  });
303
191
  };
304
192
 
305
- const bindFilesPane = (session) => {
306
- session.filesPane = createFilesPane({
307
- nav: session.nav,
308
- composer: session.composer,
309
- items: () => session.items,
310
- todoItem: () => session.todoItem,
311
- fileList: () => session.fileList(),
312
- setStatus: (value) => {
313
- session.status = value;
314
- },
315
- onQuit: () => session.onQuit(),
316
- onCheckoutBranch: session.gitBranches.onCheckoutBranch,
317
- onBranchMove: session.gitBranches.onBranchMove,
318
- onCommitMove: session.commits.onCommitMove,
319
- layout: () => session.layout,
320
- setLayout: (value) => {
321
- session.layout = value;
322
- },
323
- lastFrame: () => session.lastFrame,
324
- getSize: () => session.lastSize ?? session.getSize(),
325
- });
326
- };
327
-
328
193
  const bindLifecycle = (session) => {
329
194
  session.lifecycle = createLifecycle({
330
195
  loader: session.loader,
@@ -341,115 +206,24 @@ const bindLifecycle = (session) => {
341
206
  outdatedMap: session.outdatedMap,
342
207
  auditMap: session.auditMap,
343
208
  },
344
- watch: session.watchOpts,
345
209
  ui: session,
346
210
  });
347
211
  };
348
212
 
349
213
  const bindDispatch = (session) => {
350
- const filesPane = session.filesPane;
351
- session.dispatcher = createDispatcher({
352
- getMode: () => session.mode,
353
- getPane: () => session.pane,
354
- currentTarget: () => {
355
- if (session.pane === 'files') return session.fileCursorEntry();
356
- if (session.pane === 'branches') {
357
- return session.branches[session.branchCursor];
358
- }
359
- if (session.pane === 'commits') {
360
- const entry = session.commits.commits[session.commitCursor];
361
- const staged = targetHasStaged(session);
362
- if (!entry) return { canCommit: staged };
363
- return { ...entry, canCommit: staged };
364
- }
365
- return session.current();
366
- },
367
- composer: session.composer,
368
- changeActions: session.changeActions,
369
- commits: session.commits,
370
- gitBranches: session.gitBranches,
371
- onFileMove: filesPane.onFileMove,
372
- onBranchMove: session.gitBranches.onBranchMove,
373
- onCommitMove: session.commits.onCommitMove,
374
- onNext: filesPane.onNext,
375
- onPrev: filesPane.onPrev,
376
- onOpenFile: filesPane.onOpenFile,
377
- onFiles: filesPane.onFiles,
378
- onQuit: () => session.onQuit(),
379
- onLayout: filesPane.onLayout,
380
- onReload: session.lifecycle.onReload,
381
- onScroll: filesPane.onScroll,
382
- onHome: filesPane.onHome,
383
- onEnd: filesPane.onEnd,
384
- scrollStep: filesPane.scrollStep,
385
- });
386
- session.pointer = createPointer({
387
- nav: session.nav,
388
- getSize: () => session.lastSize ?? session.getSize(),
389
- lastFrame: () => session.lastFrame,
390
- copyText: (text, stdout) => session.copyText(text, stdout),
391
- stdout: () => session.stdout,
392
- setStatus: (value) => {
393
- session.status = value;
394
- },
395
- dispatch: (action) => session.dispatcher.dispatch(action),
396
- getPane: () => session.pane,
397
- getMode: () => session.mode,
398
- confirmChoice: (key) => session.input.confirmChoice(key),
399
- onBranch: session.gitBranches.onBranch,
400
- onOpenFile: filesPane.onOpenFile,
401
- onCheckoutBranch: session.gitBranches.onCheckoutBranch,
402
- onTodoHit: (cursor) => session.composer.onTodoHit(cursor),
403
- selectTemplate: (index) => session.selectTemplate(index),
404
- onScroll: filesPane.onScroll,
405
- });
406
- session.input = createInput({
407
- getMode: () => session.mode,
408
- getPane: () => session.pane,
409
- composer: session.composer,
410
- pointer: session.pointer,
411
- dispatch: (action) => session.dispatcher.dispatch(action),
412
- onEscape: filesPane.onEscape,
413
- onQuit: () => session.onQuit(),
414
- runComposeCommand: (result) => session.runComposeCommand(result),
415
- quit: {
416
- ready: () => session.finishQuit('ready'),
417
- editing: () => session.finishQuit('editing'),
418
- cancel: () => session.cancelQuit(),
419
- },
420
- update: {
421
- accept: session.updater.accept,
422
- decline: session.updater.decline,
423
- },
424
- drop: {
425
- confirm: () => {
426
- if (session.commits.dropName) {
427
- return void session.commits.confirmDropCommit();
428
- }
429
- session.gitBranches.confirmDropBranch();
430
- },
431
- cancel: () => {
432
- if (session.commits.dropName) {
433
- return void session.commits.cancelDropCommit();
434
- }
435
- session.gitBranches.cancelDropBranch();
436
- },
437
- },
438
- push: {
439
- force: session.gitBranches.onForcePush,
440
- cancel: session.gitBranches.cancelPush,
441
- },
442
- });
214
+ session.dispatcher = createDispatcher(session);
215
+ session.pointer = createPointer(session);
216
+ session.input = createInput(session);
443
217
  };
444
218
 
445
219
  const composeSession = (session, options) => {
446
220
  assignOptions(session, options);
447
- assignUpdater(session);
221
+ session.updater = createUpdateCoordinator(session);
448
222
  assignComposer(session);
449
223
  assignControllers(session);
450
224
  bindSessionAccessors(session);
451
225
  bindViewer(session);
452
- bindFilesPane(session);
226
+ session.filesPane = createFilesPane(session);
453
227
  bindLifecycle(session);
454
228
  bindDispatch(session);
455
229
  };
@@ -514,6 +288,10 @@ class Session {
514
288
  this.lifecycle.reloadAfterChange(afterLoad);
515
289
  }
516
290
 
291
+ ignoreWatch(ms) {
292
+ this.lifecycle.ignoreWatch(ms);
293
+ }
294
+
517
295
  applyLoaded(loaded) {
518
296
  return this.lifecycle.applyLoaded(loaded);
519
297
  }
@@ -605,7 +383,7 @@ class Session {
605
383
  }
606
384
 
607
385
  initReview() {
608
- this.review.init(this.top, this.now(), {
386
+ this.review.init(this.top, new Date(), {
609
387
  forceNew: this.forceNewReview,
610
388
  });
611
389
  }
@@ -742,10 +520,7 @@ class Session {
742
520
  }
743
521
  const onData = (chunk) => {
744
522
  this.pushInput(chunk.toString('utf8'));
745
- if (this.done) {
746
- finish();
747
- return;
748
- }
523
+ if (this.done) return void finish();
749
524
  this.draw();
750
525
  };
751
526
  this.term.startListening({
package/lib/source.js CHANGED
@@ -8,13 +8,13 @@ const selectChangeSource = (paths) => {
8
8
  if (!paths.length) return { kind: 'local', paths };
9
9
  const first = paths[0];
10
10
  const pr = parseGithubPrUrl(first);
11
- if (pr) return { kind: 'github-pr', pr, paths: paths.slice(1) };
11
+ if (pr) return { kind: 'pr', pr, paths: paths.slice(1) };
12
12
  const mr = parseGitlabMrUrl(first);
13
- if (mr) return { kind: 'gitlab-mr', mr, paths: paths.slice(1) };
13
+ if (mr) return { kind: 'mr', mr, paths: paths.slice(1) };
14
14
  return { kind: 'local', paths };
15
15
  };
16
16
 
17
- const createLoadedSource = (loaded, kind = 'github-pr', extra = {}) => {
17
+ const createLoadedSource = (loaded, kind = 'pr', extra = {}) => {
18
18
  const load = () => {
19
19
  const items = loaded.items ?? [];
20
20
  return { ...loaded, items: [...items] };
package/lib/update.js CHANGED
@@ -1,11 +1,18 @@
1
1
  'use strict';
2
2
 
3
- const { spawn } = require('node:child_process');
4
3
  const fs = require('node:fs');
5
4
  const os = require('node:os');
6
5
  const path = require('node:path');
7
6
 
8
- const { npmBin, npmOpts } = require('./sys.js');
7
+ const { jsonParse, isHashObject } = require('metautil');
8
+ const {
9
+ parseVersion,
10
+ cmpVersion,
11
+ npmBin,
12
+ npmOpts,
13
+ runProc,
14
+ oneLine,
15
+ } = require('./utilities.js');
9
16
 
10
17
  const PKG = require('../package.json');
11
18
 
@@ -29,45 +36,28 @@ const asText = (value) => (typeof value === 'string' ? value : '');
29
36
 
30
37
  const toInt = (text) => parseInt(text, 10);
31
38
 
32
- const parseVersion = (text) => {
33
- const match = /^(\d+)\.(\d+)\.(\d+)/.exec(`${text ?? ''}`);
34
- if (!match) return null;
35
- return {
36
- major: toInt(match[1]),
37
- minor: toInt(match[2]),
38
- patch: toInt(match[3]),
39
- };
40
- };
41
-
42
- const cmpVersion = (left, right) => {
43
- if (left.major !== right.major) return left.major - right.major;
44
- if (left.minor !== right.minor) return left.minor - right.minor;
45
- return left.patch - right.patch;
46
- };
47
-
48
39
  const isNpmInstall = (pkgDir) => {
49
40
  const parent = path.basename(path.dirname(pkgDir));
50
41
  return parent === 'node_modules';
51
42
  };
52
43
 
53
- const cacheFile = (env = process.env, extra = {}) => {
54
- const home = extra.home ?? os.homedir();
55
- const platform = extra.platform ?? process.platform;
44
+ const cacheFile = (env = process.env) => {
56
45
  if (env.XDG_CACHE_HOME) {
57
46
  return path.join(env.XDG_CACHE_HOME, 'reslop', 'update.json');
58
47
  }
59
- if (platform === 'win32') {
60
- const local = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
48
+ if (process.platform === 'win32') {
49
+ const local =
50
+ env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
61
51
  return path.join(local, 'reslop', 'update.json');
62
52
  }
63
- return path.join(home, '.cache', 'reslop', 'update.json');
53
+ return path.join(os.homedir(), '.cache', 'reslop', 'update.json');
64
54
  };
65
55
 
66
- const readCache = (file, readFileSync = fs.readFileSync) => {
56
+ const readCache = (file) => {
67
57
  try {
68
- const text = readFileSync(file, 'utf8');
69
- const data = JSON.parse(text);
70
- if (!data || typeof data !== 'object' || Array.isArray(data)) {
58
+ const text = fs.readFileSync(file, 'utf8');
59
+ const data = jsonParse(text);
60
+ if (!isHashObject(data)) {
71
61
  return emptyCache();
72
62
  }
73
63
  const checkedAt = toInt(data.checkedAt) || 0;
@@ -80,17 +70,15 @@ const readCache = (file, readFileSync = fs.readFileSync) => {
80
70
  }
81
71
  };
82
72
 
83
- const writeCache = (file, data, extra = {}) => {
84
- const mkdirSync = extra.mkdirSync ?? fs.mkdirSync;
85
- const writeFileSync = extra.writeFileSync ?? fs.writeFileSync;
73
+ const writeCache = (file, data) => {
86
74
  try {
87
- mkdirSync(path.dirname(file), { recursive: true });
75
+ fs.mkdirSync(path.dirname(file), { recursive: true });
88
76
  const checkedAt = data.checkedAt ?? 0;
89
77
  const compatible = data.compatible ?? '';
90
78
  const major = data.major ?? '';
91
79
  const skipped = data.skipped ?? '';
92
80
  const body = JSON.stringify({ checkedAt, compatible, major, skipped });
93
- writeFileSync(file, `${body}\n`);
81
+ fs.writeFileSync(file, `${body}\n`);
94
82
  } catch {
95
83
  // ignore cache write errors
96
84
  }
@@ -106,7 +94,7 @@ const listVersions = (data) => {
106
94
  if (!data || typeof data !== 'object') return [];
107
95
  const names = [];
108
96
  const map = data.versions;
109
- if (map && typeof map === 'object' && !Array.isArray(map)) {
97
+ if (isHashObject(map)) {
110
98
  for (const name of Object.keys(map)) names.push(name);
111
99
  }
112
100
  const tags = data['dist-tags'];
@@ -187,12 +175,6 @@ const isEnabled = (options = {}) => {
187
175
  return isNpmInstall(pkgDir);
188
176
  };
189
177
 
190
- const nowMs = (options = {}) => {
191
- if (typeof options.now === 'function') return options.now();
192
- if (typeof options.now === 'number') return options.now;
193
- return Date.now();
194
- };
195
-
196
178
  const fetchTargets = async (current, options = {}) => {
197
179
  const empty = { compatible: '', major: '' };
198
180
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -207,33 +189,18 @@ const fetchTargets = async (current, options = {}) => {
207
189
  return pickTargets(current, listVersions(data));
208
190
  };
209
191
 
210
- const npmInstall = (spec, extra = {}) =>
211
- new Promise((resolve, reject) => {
212
- const stdio = ['ignore', 'ignore', 'pipe'];
213
- const child = spawn(npmBin(), ['install', '-g', spec], npmOpts({ stdio }));
214
- let stderr = '';
215
- if (child.stderr) {
216
- child.stderr.setEncoding('utf8');
217
- child.stderr.on('data', (chunk) => {
218
- stderr += chunk;
219
- });
220
- }
221
- const timeout = extra.timeout ?? INSTALL_MS;
222
- const timer = setTimeout(() => {
223
- child.kill();
224
- reject(new Error('timed out'));
225
- }, timeout);
226
- child.on('error', (error) => {
227
- clearTimeout(timer);
228
- reject(error);
229
- });
230
- child.on('close', (status) => {
231
- clearTimeout(timer);
232
- if (status === 0) return void resolve();
233
- const line = stderr.trim().split('\n')[0];
234
- reject(new Error(line || 'npm install failed'));
235
- });
236
- });
192
+ const npmInstall = async (spec, extra = {}) => {
193
+ const timeout = extra.timeout ?? INSTALL_MS;
194
+ const result = await runProc(
195
+ npmBin(),
196
+ ['install', '-g', spec],
197
+ npmOpts({ timeout }),
198
+ );
199
+ if (result.error) throw result.error;
200
+ if (result.status !== 0) {
201
+ throw new Error(oneLine(result.stderr, 'npm install failed'));
202
+ }
203
+ };
237
204
 
238
205
  const installUpdate = async (version, options = {}) => {
239
206
  if (typeof options.install === 'function') {
@@ -246,19 +213,17 @@ const installUpdate = async (version, options = {}) => {
246
213
 
247
214
  const markSkipped = (version, options = {}) => {
248
215
  const file = options.cacheFile ?? cacheFile(options.env);
249
- const readFileSync = options.readFileSync ?? fs.readFileSync;
250
- const cache = readCache(file, readFileSync);
251
- writeCache(file, { ...cache, skipped: version }, options);
216
+ const cache = readCache(file);
217
+ writeCache(file, { ...cache, skipped: version });
252
218
  };
253
219
 
254
220
  const checkUpdate = async (options = {}) => {
255
221
  if (!isEnabled(options)) return { action: 'skip' };
256
- const now = nowMs(options);
222
+ const now = Date.now();
257
223
  const current = options.current ?? PKG_VERSION;
258
224
  const file = options.cacheFile ?? cacheFile(options.env);
259
- const readFileSync = options.readFileSync ?? fs.readFileSync;
260
225
  const interval = options.interval ?? CHECK_INTERVAL_MS;
261
- const cache = readCache(file, readFileSync);
226
+ const cache = readCache(file);
262
227
  if (isFresh(cache, now, interval)) {
263
228
  const plan = planUpdate(current, cache);
264
229
  if (plan.action === 'install') return { action: 'none' };
@@ -277,10 +242,10 @@ const checkUpdate = async (options = {}) => {
277
242
  major: targets.major,
278
243
  };
279
244
  if (!targets.compatible && !targets.major) {
280
- writeCache(file, { ...cache, checkedAt: now }, options);
245
+ writeCache(file, { ...cache, checkedAt: now });
281
246
  return { action: 'none' };
282
247
  }
283
- writeCache(file, next, options);
248
+ writeCache(file, next);
284
249
  return planUpdate(current, next);
285
250
  };
286
251