-
Notifications
You must be signed in to change notification settings - Fork 0
/
npmjs.el
6546 lines (5792 loc) · 258 KB
/
npmjs.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; npmjs.el --- Command dispatcher for npm package manager -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Karim Aziiev <karim.aziiev@gmail.com>\
;; Author: Karim Aziiev <karim.aziiev@gmail.com>
;; URL: https://github.com/KarimAziev/npmjs
;; Version: 0.2.0.50-git
;; Keywords: tools
;; Package-Requires: ((emacs "29.1") (transient "0.4.3"))
;; SPDX-License-Identifier: GPL-3.0-or-later
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Command dispatcher for npm package manager
;;; Code:
(require 'transient)
(require 'compile)
(eval-when-compile
(require 'subr-x))
(defvar json-object-type)
(defvar json-array-type)
(defvar json-null)
(defvar json-false)
(declare-function json-read-from-string "json")
(defcustom npmjs-nvm-dir (or (getenv "NVM_DIR")
(expand-file-name "~/.nvm"))
"The directory path where Node Version Manager (NVM) is installed.
Specifies the directory where Node Version Manager (NVM) is installed.
If the environment variable NVM_DIR is set, its value is used. Otherwise, the
default value is the `.nvm' directory in the user's home directory.
This should be a valid directory path.
To change the value, customize it through the customization interface or set it
in the Emacs configuration file using `setq'.
For example:
```elisp \\=(setq npmjs-nvm-dir \"/path/to/custom/nvm\") ```
Ensure that the specified directory exists and contains the NVM installation for
proper integration with npm-related functionality.
Belongs to the `npmjs' customization group.
See https://github.com/nvm-sh/nvm."
:group 'npmjs
:type 'directory)
(defcustom npmjs-inhibit-prefix-cache nil
"Set to non-nil to disable caching of package prefix requests.
Determines whether to bypass the local cache when fetching package information
from the npm registry.
When non-nil, each request to the npm registry will ignore the locally cached
data and retrieve fresh information. This can be useful when working with
frequently updated packages or when debugging network-related issues.
To enable this feature, set the value to t. To rely on the local cache for
improved performance, leave the value as nil.
This is a boolean variable. Toggle the caching behavior by setting the variable
accordingly."
:group 'npmjs
:type 'boolean)
(defcustom npmjs-nvm-download-url "https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh"
"The URL to download the nvm installation script.
Specifies the URL from which the Node Version Manager (nvm) install script
should be downloaded.
This should be a STRING representing a valid URL pointing to the nvm install
script.
Changing this value is useful for directing the download process to a different
version of nvm or to a forked repository.
Ensure that the provided URL is accessible and points to a legitimate nvm
install script.
The default value points to the official nvm install script for version v0.39.3.
To apply changes to this variable, use the `customize-set-variable' function or
set it directly in an Emacs Lisp file."
:type 'string
:group 'npmjs)
(defcustom npmjs-omit-commmands '("stars"
"star"
"completion"
"bugs"
"help"
"help-search"
"fund"
"search"
"explore"
"unstar"
"pkg")
"A list of npm commands to exclude from the npmjs interface.
A list of strings representing npm commands to omit from execution.
Each element in the list should be a string that matches the name of an npm
command that is not desired to be run.
Commands listed here will be excluded from certain operations when interacting
with npm through Emacs tooling.
To modify this list, use the customization interface or set the variable
directly in Emacs Lisp code with `setq'.
For example, to omit the \"install\" and \"update\" commands, set the variable
to \\='(\"install\" \"update\").
Changes to this variable will take effect the next time npm-related functions
are invoked."
:group 'npmjs
:type '(repeat string))
(defcustom npmjs-compile-command 'npmjs
"Set the command to compile JavaScript projects using npmjs.
Specifies the command to be used for compiling JavaScript projects managed by
npm.
The value should be a symbol referring to a function that will be called to
perform the compilation process.
To change the compile command, set this variable to the desired function symbol
using `setq' or customize the NPMJS group.
The function bound to this symbol should take no arguments and will be executed
in the context of the project's root directory."
:group 'npmjs
:type 'function)
(defcustom npmjs-repeat-compile-command 'npmjs-repeat
"Set the command to repeat compilation in the npmjs package.
Specifies the function to be called repeatedly when compiling a project using
npm.
The value should be a symbol referring to a function that takes no arguments and
is responsible for running the desired compile command.
To use, set this variable to the function that invokes the npm build process for
the project.
For example, if there is a function named `my-npm-compile', assign it like so:
\\=(setq npmjs-repeat-compile-command \\='my-npm-compile)
After setting, the specified function will be executed whenever the repeat
compile command is triggered within the npmjs package context."
:group 'npmjs
:type 'function)
(defcustom npmjs-started-hook nil
"Functions to run after the npmjs process has started.
A hook that gets run after npmjs operations are initiated.
Hooks are lists of functions to be called at specific times.
Functions added to this hook can be used to perform additional actions when
npmjs processes start.
To add a function to this hook, use `add-hook'.
For example: \\='(add-hook \\='npmjs-started-hook \\=#'my-custom-function)`.
Ensure that functions intended for this hook do not take any arguments and do
not rely on the return value."
:group 'npmjs
:type 'hook)
(defcustom npmjs-finished-hook nil
"Functions to run after npmjs operations complete.
A hook that gets run after npmjs operations are completed.
Hooks are lists of functions to be called at specific times. When a npmjs
operation finishes, each function in this hook is called with no arguments.
To add a function to this hook, use `add-hook'. For example:
\\=(add-hook \\='npmjs-finished-hook \\='my-npmjs-finished-function)
Where `my-npmjs-finished-function' is the name of the function to be called.
Functions can be removed from the hook using `remove-hook'.
This hook is useful for performing cleanup, notifications, or other tasks that
should occur after npmjs operations."
:group 'npmjs
:type 'hook)
(defcustom npmjs-setup-hook nil
"Functions to run after setting up the npmjs environment.
A list of functions to be called after setting up the npmjs environment.
Each function is called with no arguments and is useful for customizing the
behavior of npmjs-related commands and interactions.
To add a function to this hook, use `add-hook'. For example:
\\=(add-hook \\='npmjs-setup-hook \\='my-npmjs-setup-function)
Where `my-npmjs-setup-function' is the name of the function to be added.
This hook is typically used for tasks such as configuring npmjs settings,
initializing npmjs projects, or integrating with other tools and packages."
:group 'npmjs
:type 'hook)
(defcustom npmjs-message-function 'message
"The function to use for displaying messages in the npmjs package.
Specifies the function to use for displaying messages from npmjs operations.
The value should be a function that accepts the same arguments as MESSAGE, or
nil to suppress messages.
To use a custom function, set the value to a symbol that names the function.
For example, to use a function named `my-npmjs-message-function', set the value
like this:
```elisp \\=(setq npmjs-message-function \\='my-npmjs-message-function) ```
To suppress all messages, set the value to nil:
```elisp \\=(setq npmjs-message-function nil) ```
The default value is MESSAGE, which displays messages in the echo area."
:type '(choice
(const :tag "None" nil)
(function :tag "Function"))
:group 'npmjs)
(defcustom npmjs-use-comint-in-scripts t
"Whether to enable running package.json scripts in comint mode.
When non-nil, scripts are run in an interactive comint buffer, allowing for
input and interactive command execution.
If set to nil, scripts are executed using the `compile' command, which is
non-interactive. This option can be toggled to switch between interactive and
non-interactive script execution modes.
The variable is a boolean value.
To change the setting, customize the variable accordingly."
:type '(boolean)
:group 'npmjs)
(defcustom npmjs-compilation-error-regexp-alist '(("\\(warning: .*\\)? at \\([^ \n]+\\):\\([0-9]+\\):\\([0-9]+\\)$"
2
3
4
(1))
("^\\(?:[ \t]+at \\|==[0-9]+== +\\(?:at\\|b\\(y\\)\\)\\).+(\\([^()\n]+\\):\\([0-9]+\\):\\([0-9]+\\))$"
2
3
4
(1))
("^\\(\\(\\[[a-z]+\\]?\\)[ ]*\\)?\\(ERROR\\)\s\\(in\\|at\\)[\s\t]+\\([^ \n]+\\)[(]\\([0-9]+\\)[:,]\\([0-9]+\\)[)]$"
5
6
7
(1)))
"Alist that specifies how to match errors in compiler output.
This is like `compilation-error-regexp-alist', but for `npmjs-compilation-mode'
only."
:group 'npmjs
:type '(repeat (sexp :tag "Error specification")))
(defvar-local npmjs--current-command nil)
(defvar-local npmjs--current-node-version nil)
(defvar-local npmjs-current-descriptions-alist nil)
(defvar npmjs-descriptions-alist nil)
;; common utilities
(defun npmjs-message (string &rest arguments)
"Display a formatted message prefixed with \"npmjs: \".
Argument STRING is the format string for the message.
Remaining ARGUMENTS arguments are objects substituted into STRING according to
the format specifications."
(when npmjs-message-function
(funcall npmjs-message-function
(concat "npmjs: " (apply #'format string arguments)))))
(defun npmjs--plist-remove-nils (plist)
"Remove nil values from a property list.
Argument PLIST is a property list where each even element is a key and the
following odd element is its value."
(let* ((result (list 'head))
(last result))
(while plist
(let* ((key (pop plist))
(val (pop plist))
(new (and val (list key val))))
(when new
(setcdr last new)
(setq last (cdr new)))))
(cdr result)))
(defun npmjs-unquote (exp)
"Remove `function' symbols from list start.
Argument EXP is an expression to be unquoted."
(declare (pure t)
(side-effect-free t))
(while (memq (car-safe exp) '(quote function))
(setq exp (cadr exp)))
exp)
(eval-and-compile
(defun npmjs--expand (init-fn)
"Expand the given macro and return the expanded form.
Argument INIT-FN is the macro to be expanded."
(setq init-fn (macroexpand init-fn))
(if (symbolp init-fn)
`(#',init-fn)
`(,init-fn))))
(defmacro npmjs--compose (&rest functions)
"Compose FUNCTIONS into a single callable chain.
Remaining arguments FUNCTIONS are Lisp functions to be composed."
(declare (debug t)
(pure t)
(indent defun)
(side-effect-free t))
(setq functions (reverse functions))
(let ((args-var (make-symbol "arguments")))
`(lambda (&rest ,args-var)
,@(let ((init-fn (pop functions)))
(list
(seq-reduce
(lambda (acc fn)
`(funcall ,@(npmjs--expand fn) ,acc))
functions
`(apply ,@(npmjs--expand init-fn) ,args-var)))))))
(defmacro npmjs--const (value)
"Define a constant-returning function.
Argument VALUE is the value to be returned by the generated function."
(declare (pure t)
(side-effect-free error-free))
(let ((arg (make-symbol "_")))
`(lambda (&rest ,arg) ,value)))
(defmacro npmjs--cond (&rest pairs)
"Transform conditions into a lambda function.
Remaining arguments PAIRS are lists where each list contains a condition and a
result form."
(declare (pure t)
(indent defun)
(side-effect-free error-free))
(setq pairs (mapcar (lambda (it)
(if (listp it)
(apply #'vector it)
it))
pairs))
(let ((args (make-symbol "arguments")))
`(lambda (&rest ,args)
(cond ,@(mapcar (lambda (v)
(list (if (eq (aref v 0) t) t
`(apply ,@(npmjs--expand (aref v 0)) ,args))
`(apply ,@(npmjs--expand (aref v 1)) ,args)))
pairs)))))
(defmacro npmjs--or (&rest functions)
"Combine FUNCTIONS with logical OR, returning first truthy result.
Remaining arguments FUNCTIONS are Lisp expressions or symbols representing
FUNCTIONS to be called with a single argument."
(declare (debug t)
(pure t)
(side-effect-free t))
(let ((it (make-symbol "it")))
`(lambda (,it)
(or
,@(mapcar (lambda (v)
(if (symbolp v)
`(,v ,it)
`(funcall ,v ,it)))
functions)))))
(defmacro npmjs--and (&rest functions)
"Combine FUNCTIONS with logical AND on input `it'.
Remaining arguments FUNCTIONS are Lisp expressions or symbols representing
FUNCTIONS to be called with a single argument `it'."
(declare (debug t)
(pure t)
(side-effect-free t))
`(lambda (it)
(and
,@(mapcar (lambda (v)
(if (symbolp v)
`(,v it)
`(funcall ,v it)))
functions))))
(defmacro npmjs--rpartial (fn &rest args)
"Create a reversed partial function with fixed arguments.
Argument FN is a function or a symbol referring to a function.
Remaining arguments ARGS are the arguments to be appended to the call to FN when
the resulting lambda is invoked."
(declare (side-effect-free t))
`(lambda (&rest pre-args)
,(car (list (if (symbolp fn)
`(apply #',fn (append pre-args (list ,@args)))
`(apply ,fn (append pre-args (list ,@args))))))))
(defmacro npmjs--converge (combine-fn &rest functions)
"Apply a combination function to results of multiple functions.
Argument COMBINE-FN is a function that combines the results of other functions.
Remaining arguments FUNCTIONS are functions whose results are combined by
COMBINE-FN. If the first argument in FUNCTIONS is a vector, it is converted to a
list before being processed."
`(lambda (&rest args)
(apply
,@(npmjs--expand combine-fn)
(list
,@(mapcar (lambda (v)
`(apply ,@(npmjs--expand v) args))
(if (vectorp (car functions))
(append (car functions) nil)
functions))))))
(defun npmjs-set-env-vars (version &optional globaly)
"Set Node.js environment variables based on version.
Argument VERSION is a string representing the Node.js version to set environment
variables for.
Optional argument GLOBALY is a boolean flag indicating whether to set the
environment variables globally or locally. If non-nil, variables are set
globally; otherwise, they are set locally."
(let ((vars
(npmjs-nvm-get-env-vars version)))
(pcase-dolist (`(,var ,value) vars)
(setenv var value)
(when (string-equal "PATH" var)
(if globaly
(setq-default exec-path (append (parse-colon-path value) (list exec-directory))
eshell-path-env value)
(setq-local exec-path (append (parse-colon-path value) (list exec-directory))))))))
(defmacro npmjs-nvm-with-env-vars (version &rest body)
"Set environment variables for a specific Node.js version.
Argument VERSION is a string specifying the Node.js version to use.
Remaining arguments BODY are the forms to be executed with the Node.js VERSION
environment variables set."
(declare
(indent defun))
`(let ((process-environment (copy-sequence process-environment)))
(npmjs-set-env-vars ,version)
(let ((npmjs--current-node-version ,version))
,@body)))
(defmacro npmjs-with-temp-buffer (&rest body)
"Execute BODY with temporary buffer and Node.js version.
Remaining arguments BODY are Lisp expressions that are evaluated in the context
of the temporary buffer with Node.js version environment variables set."
(let ((version (make-symbol "version")))
`(let ((,version npmjs--current-node-version))
(with-temp-buffer
(npmjs-nvm-with-env-vars
(setq npmjs--current-node-version
,version)
,@body)))))
(defmacro npmjs-parse-help-with-output (output &rest body)
"Parse npm help OUTPUT in a temporary buffer.
Argument OUTPUT is a string containing the help output to parse.
Remaining arguments BODY are forms that are evaluated with the OUTPUT inserted
into a temporary buffer."
(declare (indent 1)
(debug t))
`(npmjs-with-temp-buffer
(save-excursion
(insert ,output))
,@body))
(defun npmjs-exec-with-args (command &rest args)
"Execute npm COMMAND with arguments and return output.
Argument COMMAND is a string representing the npm command to execute.
Remaining arguments ARGS are strings that represent additional arguments to pass
to the npm command."
(let ((cmdline (mapconcat (lambda (it)
(if (string-match-p "[\s\t\n]" it)
(shell-quote-argument it)
it))
(append (list command)
(flatten-list args))
"\s")))
(with-temp-buffer
(shell-command cmdline (current-buffer))
(let ((output (string-trim (buffer-string))))
(unless (string-empty-p output)
output)))))
;; nvm utilities
(defconst npmjs-nvm-version-re
"v[0-9]+\\.[0-9]+\\.[0-9]+"
"Regex matching a Node version.")
(defmacro npmjs-nvm-with-current-node-version (&rest body)
"Set environment variables and run BODY with Node.js version.
Remaining arguments BODY are forms that are evaluated with the Node.js version
environment variables set."
`(npmjs-nvm-with-env-vars
(setq npmjs--current-node-version
(or npmjs--current-node-version
(if (npmjs-nvm-path)
(npmjs-confirm-node-version)
(replace-regexp-in-string "^v" ""
(car
(process-lines
"node"
"-v"))))))
,@body))
(defun npmjs-nvm-path ()
"Set Node Version Manager path from environment or home directory."
(when-let* ((nvm-dir (or (getenv "NVM_DIR")
(when (file-exists-p "~/.nvm/")
"~/.nvm/")))
(file (expand-file-name "nvm.sh" nvm-dir)))
(when (file-exists-p file)
file)))
(defvar npmjs-nvm-remote-node-versions-alist nil)
(defun npmjs-nvm-ls-remote (&rest args)
"List remote Node versions using `nvm ls-remote`.
Remaining arguments ARGS are strings that represent additional arguments to pass
to the `nvm ls-remote` command."
(when-let* ((nvm-path (npmjs-nvm-path))
(node-versions
(apply #'npmjs-exec-with-args "source" nvm-path
"&&" "nvm"
"ls-remote"
"--no-colors"
args)))
(nreverse
(mapcar
(lambda (it)
(let* ((parts (delete "*" (split-string it nil t)))
(found (seq-find
(apply-partially
#'string-match-p
npmjs-nvm-version-re)
parts))
(meta (string-join (seq-drop (member found parts) 1) "\s")))
(cons found meta)))
(split-string
node-versions
"[\n]" t)))))
(defun npmjs-nvm-read-remote-node-version ()
"Fetch and display Node.js versions for selection."
(when-let* ((node-versions
(or npmjs-nvm-remote-node-versions-alist
(setq npmjs-nvm-remote-node-versions-alist
(npmjs-nvm-ls-remote)))))
(let* ((installed (mapcar #'car (npmjs-nvm--installed-versions)))
(default-global (npmjs-nvm-strip-prefix
(npmjs-current-default-node-version)))
(current-global
(when (get-buffer (npmjs--get-global-buffer-name))
(npmjs-nvm-strip-prefix
(buffer-local-value
'npmjs--current-node-version
(get-buffer
(npmjs--get-global-buffer-name))))))
(project-node
(npmjs-nvm-strip-prefix
(npmjs-nvm-get-nvmrc-required-node-version)))
(curr-node (npmjs-nvm-strip-prefix
npmjs--current-node-version))
(items (mapcar (lambda (it)
(seq-copy (car it)))
node-versions))
(annotf
(lambda (item)
(let ((it (npmjs-nvm-strip-prefix item)))
(let ((status
(cond ((equal it default-global)
"(System global)")
((equal it current-global)
"(Current global)")
((equal it project-node)
"(nvmrc) ")
((equal it curr-node)
"(Buffer local node) ")
((member item installed)
"Installed")))
(meta (cdr (assoc item node-versions))))
(concat " "
(string-join (delete nil (list status
meta))
"\s")))))))
(completing-read "Version: "
(lambda (str pred action)
(if (eq action 'metadata)
`(metadata
(annotation-function . ,annotf))
(complete-with-action action
items
str pred)))))))
(defun npmjs-nvm-strip-prefix (version)
"Remove prefix from VERSION string if it matches a regex.
Argument VERSION is a string representing the version number to be processed."
(if (and version (string-match-p npmjs-nvm-version-re version))
(substring-no-properties version 1)
version))
(defun npmjs-expand-when-exists (filename &optional directory)
"Expand and return file path if it exists.
Argument FILENAME is the name of the file to expand.
Optional argument DIRECTORY is the directory to prepend to FILENAME; defaults to
the current DIRECTORY if not specified."
(let ((file (expand-file-name filename directory)))
(when (file-exists-p file)
file)))
(defun npmjs-nvm--installed-versions-dirs ()
"List Node.js versions installed by nvm."
(let* ((files (mapcan
(lambda (versions-dir)
(directory-files versions-dir t
directory-files-no-dot-files-regexp))
(delq nil
(list
(npmjs-expand-when-exists
npmjs-nvm-dir)
(npmjs-expand-when-exists
"versions"
npmjs-nvm-dir))))))
(mapcan
(lambda (it)
(when-let* ((name
(when (file-directory-p it)
(file-name-nondirectory
(directory-file-name
it)))))
(if (string-match-p (concat npmjs-nvm-version-re "$")
name)
(list it)
(directory-files it t npmjs-nvm-version-re))))
files)))
(defun npmjs-nvm--installed-versions ()
"List Node.js versions installed via nvm."
(mapcar (lambda (it)
(cons (file-name-nondirectory it) it))
(npmjs-nvm--installed-versions-dirs)))
(defun npmjs-nvm--version-from-string (version-string)
"Extract numeric parts from a version string.
Argument VERSION-STRING is a string representing a version number, which is
split and converted to a list of numbers."
(mapcar #'string-to-number (split-string version-string "[^0-9]" t)))
(defun npmjs-nvm--version-match-p (matcher version)
"Check if VERSION lists match or start with nil.
Argument MATCHER is a list representing the VERSION pattern to match against.
Argument VERSION is a list representing the version to be checked for a match."
(or (eq (car matcher) nil)
(and (eq (car matcher)
(car version))
(npmjs-nvm--version-match-p (cdr matcher)
(cdr version)))))
(defun npmjs-nvm-version-compare (a b)
"Compare version lists recursively.
Argument A is a list representing a version number, where each element is a part
of the version to be compared.
Argument B is A list representing another version number, structured similarly
to A for comparison purposes."
(if (eq (car a)
(car b))
(npmjs-nvm-version-compare (cdr a)
(cdr b))
(< (car a)
(car b))))
(defun npmjs-nvm-find-exact-version-for (short)
"Find exact Node.js version match.
Argument SHORT is a string representing a version number, which may or may not
start with \"v\", \"node\", or \"iojs\"."
(when (and short
(string-match-p "v?[0-9]+\\(\\.[0-9]+\\(\\.[0-9]+\\)?\\)?$" short))
(unless (or (string-prefix-p "v" short)
(string-prefix-p "node" short)
(string-prefix-p "iojs" short))
(setq short (concat "v" short)))
(let* ((versions (npmjs-nvm--installed-versions))
(requested (npmjs-nvm--version-from-string short))
(first-version
(seq-find (lambda (it)
(string= (car it) short))
versions)))
(or
first-version
(let ((possible-versions
(seq-filter
(lambda (version)
(npmjs-nvm--version-match-p
requested
(npmjs-nvm--version-from-string (car version))))
versions)))
(when possible-versions
(car (sort possible-versions
(lambda (a b)
(not (npmjs-nvm-version-compare
(npmjs-nvm--version-from-string
(car a))
(npmjs-nvm--version-from-string
(car b)))))))))))))
(defun npmjs-nvm-get-nvmrc-required-node-version ()
"Retrieve Node.js version from project's `.nvmrc' file."
(when-let* ((nvmrc (locate-dominating-file default-directory ".nvmrc")))
(with-temp-buffer (insert-file-contents
(expand-file-name ".nvmrc" nvmrc))
(string-trim
(buffer-substring-no-properties (point-min)
(point-max))))))
(defun npmjs-nvm-read-installed-node-versions ()
"List installed Node.js versions for selection."
(let ((alist (npmjs-nvm--installed-versions))
(default-version (string-trim (shell-command-to-string "node -v")))
(choice))
(setq choice (completing-read
"Version: "
(lambda (str pred action)
(if
(eq action 'metadata)
`(metadata
(annotation-function .
(lambda
(it)
(when (equal
it
,default-version)
" (default)"))))
(complete-with-action action
alist
str pred)))
nil t))
(cons choice (cdr (assoc choice alist)))))
(defun npmjs-nvm-get-env-vars (version)
"Extract environment variables for a given Node version.
Argument VERSION is a string representing the Node.js version to get environment
variables for."
(when-let* ((version-path (cdr (npmjs-nvm-find-exact-version-for version))))
(let* ((env-flags
(mapcar
(lambda (it)
(list
(car it)
(concat version-path "/" (cadr it))))
'(("NVM_BIN" "bin")
("NVM_PATH" "lib/node")
("NVM_INC" "include/node"))))
(path-re (concat "^"
(concat
(or (getenv "NVM_DIR")
(expand-file-name "~/.nvm"))
"/\\(?:versions/node/\\|versions/io.js/\\)?")
"v[0-9]+\\.[0-9]+\\.[0-9]+" "/bin/?$"))
(new-bin-path (expand-file-name "bin/" version-path))
(paths
(cons
new-bin-path
(seq-remove
(lambda (path)
(if path (string-match-p path-re path) t))
(parse-colon-path (getenv "PATH")))))
(new-path (list "PATH" (string-join paths path-separator)))
(flags (append env-flags (list new-path))))
flags)))
(defun npmjs-nvm-get-env-for-node (version)
"Retrieve Node version-specific environment variables.
Argument VERSION is a string representing the Node.js version to get the
environment for."
(when-let* ((flags (npmjs-nvm-get-env-vars version)))
(let ((regexp (mapconcat #'identity (mapcar #'car flags)
"\\|")))
(append (mapcar (lambda (it)
(concat (car it) "=" (cadr it)))
flags)
(seq-remove (apply-partially #'string-match-p regexp)
process-environment)))))
(defun npmjs-current-default-node-version ()
"Fetch and format the current Node.js version installed."
(let ((default-directory (expand-file-name "~/")))
(npmjs-nvm-strip-prefix
(string-trim
(shell-command-to-string
"node -v")))))
(defun npmjs-confirm-node-version (&optional all)
"Check and select Node.js version.
Optional argument ALL is a boolean; if non-nil, all installed Node versions are
considered."
(let* ((installed
(when all
(mapcar (lambda (it)
(npmjs-nvm-strip-prefix (car it)))
(npmjs-nvm--installed-versions))))
(default-global (npmjs-current-default-node-version))
(current-global
(when (get-buffer (npmjs--get-global-buffer-name))
(buffer-local-value 'npmjs--current-node-version
(get-buffer
(npmjs--get-global-buffer-name)))))
(project-node
(npmjs-nvm-get-nvmrc-required-node-version))
(curr-node npmjs--current-node-version)
(cands
(seq-uniq
(mapcar #'npmjs-nvm-strip-prefix
(append
(delq nil
(list
default-global
current-global
project-node
curr-node))
installed))))
(annotf (lambda (it)
(cond ((equal it default-global)
" Global")
((equal it current-global)
" Current global")
((equal it project-node)
" required by (nvmrc)")
((equal it curr-node)
" Buffer local node ")))))
(setq npmjs--current-node-version
(if (<= (length cands) 1)
(car cands)
(completing-read "Which node to use?"
(lambda (str pred
action)
(if (eq action
'metadata)
`(metadata
(annotation-function
.
,annotf)
(display-sort-fn .
,(lambda (it)
(seq-sort-by
(or
(cdr
(assoc-string
it
'((project-node . 1)
(default-global . 2)
(current-global . 3)
(curr-node . 4))))
5)
#'< it))))
(complete-with-action
action
cands
str
pred))))))))
(defvar npmjs-node-installing nil)
;;;###autoload
(defun npmjs-nvm-install-node-version (version &optional args)
"Install a specified Node.js VERSION using NVM.
Argument VERSION is a string representing the Node.js version to install.
Optional argument ARGS is a list of additional arguments to pass to the Node.js
installation command."
(interactive
(let* ((targs (transient-args transient-current-command))
(version
(if (not (member "--lts" targs))
(progn (setq npmjs-nvm-remote-node-versions-alist
(npmjs-nvm-ls-remote))
(npmjs-nvm-strip-prefix
(npmjs-nvm-read-remote-node-version)))
(setq npmjs-nvm-remote-node-versions-alist
(npmjs-nvm-ls-remote "--lts"))
(prog1 (npmjs-nvm-strip-prefix
(npmjs-nvm-read-remote-node-version))))))
(list version targs)))
(when-let* ((nvm-path (npmjs-nvm-path)))
(if (member version (mapcar (lambda (it)
(npmjs-nvm-strip-prefix (car it)))
(npmjs-nvm--installed-versions)))
(setq npmjs--current-node-version version)
(let ((cmd (read-string "Run?" (string-join
(delq nil (append (list "source" nvm-path
"&&"
"nvm"
"install"
version)
(remove "--lts" args)))
"\s"))))
(setq npmjs-node-installing t)
(when transient-current-command
(transient-setup transient-current-command))
(npmjs-exec-in-dir
cmd
default-directory
(lambda (&rest _)
(setq npmjs-node-installing nil)
(setq npmjs--current-node-version version)
(when transient-current-command
(transient-setup transient-current-command)))
(lambda ()
(setq npmjs-node-installing nil)
(when transient-current-command
(transient-setup transient-current-command))
(message "Error installing %s" version)))))))
(defun npmjs--write-nvm-file (version)
"Create or update the `.nvmrc' file with the given VERSION.
Argument VERSION is a string representing the Node.js version to write into the
.nvmrc file."
(when-let* ((dir (or