diff --git a/README.md b/README.md index 0d71650..606ca6b 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,13 @@ To automatically run it when opening a new buffer: (eval-after-load 'js-mode '(add-hook 'js-mode-hook #'add-node-modules-path)) ``` + +## Monorepo Support +In a monorepo scenario it might make sense to add multiple directories. +To achieve this, additional commands can be specified: + +``` +(use-package add-node-modules-path + :custom + (add-node-modules-path-command '("pnpm bin" "pnpm bin -w"))) +``` \ No newline at end of file diff --git a/add-node-modules-path.el b/add-node-modules-path.el index 9789c23..f1672c1 100644 --- a/add-node-modules-path.el +++ b/add-node-modules-path.el @@ -29,7 +29,7 @@ ;; '(add-hook 'js2-mode-hook #'add-node-modules-path)) ;;; Code: - +(require 'seq) (require 's) (defgroup add-node-modules-path nil @@ -38,9 +38,15 @@ :group 'environment) ;;;###autoload -(defcustom add-node-modules-path-command "npm bin" - "Command to find the bin path." - :type 'string) +(defcustom add-node-modules-path-command '("npm bin") + "Command(s) to find the bin path. To add multiple bin paths, simply add +multiple commands to the list, e.g. \\='(\"pnpm bin\" \"pnpm bin -w\")" + :type '(repeat string) + :set (lambda (symbol value) + "Converts a non-list value to a single-element list of the same value. +This is necessary to be backward compatible, since previous versions of this +custom var were of type string." + (set-default symbol (if (listp value) value (list value))))) ;;;###autoload (defcustom add-node-modules-path-debug nil @@ -48,29 +54,69 @@ :type 'boolean :group 'add-node-modules-path) +(defun add-node-modules-path/trim-list-and-elements (list) + "Trims all string values in LIST and empty / non-string values are removed." + (if (listp list) + (seq-filter 's-present? (mapcar 's-trim (seq-filter 'stringp list))))) + +(defun add-node-modules-path/exec-command (command) + "Executes the given COMMAND and returns a plist containing the command, +its shell execution result and a boolean indicating, whether the execution +result denotes a valid directory" + (if (and (stringp command) (s-present? command)) + (let ((result (s-chomp (shell-command-to-string command)))) + (list 'command command 'result result 'directory-p (file-directory-p result))))) + +(defun add-node-modules-path/exec-command-list (command-list) + "Executes all commands in COMMAND-LIST and returns a list of plists +containing the various command execution results. Elements in COMMAND-LIST which +are not strings are ignoredand will not appear in the result." + (if (listp command-list) + (seq-filter 'consp (mapcar 'add-node-modules-path/exec-command command-list)))) + +(defun add-node-modules-path/get-valid-directories (command-executions) + "Filters the provided COMMAND-EXECUTIONS for entries, whose execution result +denotes an existing directory" + (if (listp command-executions) + (let ((filtered (seq-filter '(lambda (elt) (plist-get elt 'directory-p)) command-executions))) + (mapcar #'(lambda (elt) (plist-get elt 'result)) filtered)))) + +(defun add-node-modules-path/get-invalid-executions (command-executions) + "Filters the provided COMMAND-EXECUTIONS for entries, whose execution result +denotes an invalid or non-existing directory" + (if (listp command-executions) + (seq-filter #'(lambda (elt) (and (plist-member elt 'directory-p) (not (plist-get elt 'directory-p)))) command-executions))) + +(defun add-node-modules-path/warn-about-failed-executions (command-executions) + "Displays warnings about all failed COMMAND-EXECUTIONS." + (let ((failed (add-node-modules-path/get-invalid-executions command-executions))) + (dolist (elt failed) + (let ((cmd (plist-get elt 'command)) + (path (plist-get elt 'result))) + (display-warning 'add-node-modules-path (format-message "Failed to run `%s':\n %s" cmd path)))))) + +(defun add-node-modules-path/add-to-list-multiple (list to-add) + "Adds multiple items to LIST." + (dolist (item to-add) + (add-to-list list item))) + ;;;###autoload (defun add-node-modules-path () "Run `npm bin` command and add the path to the `exec-path`. If `npm` command fails, it does nothing." (interactive) - - (let* ((res (s-chomp (shell-command-to-string add-node-modules-path-command))) - (exists (file-exists-p res)) - ) - (cond - (exists - (make-local-variable 'exec-path) - (add-to-list 'exec-path res) - (when add-node-modules-path-debug - (message "Added to `exec-path`: %s" res)) - ) - (t - (when add-node-modules-path-debug - (message "Failed to run `%s':\n %s" add-node-modules-path-command res)) - )) - ) - ) - + (let* ((commands (add-node-modules-path/trim-list-and-elements add-node-modules-path-command)) + (executions (add-node-modules-path/exec-command-list commands)) + (dirs (add-node-modules-path/get-valid-directories executions))) + (if (length> dirs 0) + (progn + (make-local-variable 'exec-path) + (add-node-modules-path/add-to-list-multiple 'exec-path (reverse dirs)) + (if add-node-modules-path-debug + (message "Added to `exec-path`: %s" (s-join ", " dirs))))) + (if add-node-modules-path-debug + (add-node-modules-path/warn-about-failed-executions executions)))) + (provide 'add-node-modules-path) ;;; add-node-modules-path.el ends here diff --git a/test/add-node-modules-path-test.el b/test/add-node-modules-path-test.el new file mode 100644 index 0000000..f83f41c --- /dev/null +++ b/test/add-node-modules-path-test.el @@ -0,0 +1,87 @@ +;; +;; +;; + +(ert-deftest add-node-modules-path/trim-list-and-elements-test () + (should (equal (add-node-modules-path/trim-list-and-elements '("pnpm bin" "pnpm bin -w" "ls -al ")) + '("pnpm bin" "pnpm bin -w" "ls -al"))) + (should (equal (add-node-modules-path/trim-list-and-elements '(" pnpm bin -w")) '("pnpm bin -w"))) + (should (equal (add-node-modules-path/trim-list-and-elements '(nil "" " ls -al " "" nil "" nil)) '("ls -al"))) + (should (eq (add-node-modules-path/trim-list-and-elements "") nil)) + (should (eq (add-node-modules-path/trim-list-and-elements " ") nil)) + (should (eq (add-node-modules-path/trim-list-and-elements 1701) nil)) + (should (eq (add-node-modules-path/trim-list-and-elements "a string") nil))) + +(ert-deftest add-node-modules-path/exec-command-test () + (should (eq (add-node-modules-path/exec-command nil) nil)) + (should (eq (add-node-modules-path/exec-command "") nil)) + (should (eq (add-node-modules-path/exec-command 3) nil)) + (should (eq (add-node-modules-path/exec-command 'a-symbol) nil)) + (if (equal system-type 'gnu/linux) + (let ((res (add-node-modules-path/exec-command "echo \"/usr/bin\""))) + (should (equal (plist-get res 'command) "echo \"/usr/bin\"")) + (should (equal (plist-get res 'result) "/usr/bin")) + (should (equal (plist-get res 'directory-p) t)))) + (if (equal system-type 'gnu/linux) + (let ((res (add-node-modules-path/exec-command "echo \"ls -al\""))) + (should (equal (plist-get res 'command) "echo \"ls -al\"")) + (should (equal (plist-get res 'result) "ls -al")) + (should (equal (plist-get res 'directory-p) nil))))) + +(ert-deftest add-node-modules-path/exec-command-list-test () + (let ((should-produce-nil '(nil () 42 "a string" (1 2 3)))) + (dolist (elt should-produce-nil) + (should (eq (add-node-modules-path/exec-command-list elt) nil))))) + +(ert-deftest add-node-modules-path/get-valid-directories-test () + (let ((should-produce-nil '(nil () 1 "str" (1 2 3) (('directory-p nil) ('directory-p nil)))) + (test-data '( + (((directory-p t result "/usr/bin")) ("/usr/bin")) + (((directory-p t result "/home") (directory-p t result "/usr")) ("/home" "/usr")) + (((directory-p nil result "/not-a-valid-dir") (directory-p t result "/usr")) ("/usr")) + ))) + (dolist (elt should-produce-nil) + (should (eq (add-node-modules-path/get-valid-directories elt) nil))) + (dolist (elt test-data) + (should (equal (add-node-modules-path/get-valid-directories (car elt)) (cadr elt)))))) + +(ert-deftest add-node-modules-path/get-invalid-executions-test () + (let ((should-produce-nil '(nil () 1 "str" (('directory-p t) ('directory-p t)))) + (test-data '( + (((directory-p nil result "/usr/bin")) ((directory-p nil result "/usr/bin"))) + (((directory-p t result "/home") (directory-p nil result "/usr")) ((directory-p nil result "/usr"))) + (((directory-p nil result "/xxx") (directory-p t result "/usr")) ((directory-p nil result "/xxx"))) + ))) + (dolist (elt should-produce-nil) + (should (eq (add-node-modules-path/get-invalid-executions elt) nil))) + (dolist (elt test-data) + (should (equal (add-node-modules-path/get-invalid-executions (car elt)) (cadr elt)))))) + +(defun add-node-modules-path/exec-add-node-modules-path-test (command additions) + ;; remove any local binding of EXEC-PATH, if present + (kill-local-variable 'exec-path) + ;; prepare environment + (make-local-variable 'add-node-modules-path-debug) + (setq add-node-modules-path-debug nil) + (make-local-variable 'add-node-modules-path-command) + (setq add-node-modules-path-command command) + ;; run interactive command, which will create local binding of EXEC-PATH and add to it + (add-node-modules-path) + ;; checks + (should (eq (local-variable-p 'exec-path) t)) + (let ((i 0)) + (dolist (elt additions) + (should (equal (nth i exec-path) elt)) + (setq i (1+ i)))) + ;; env cleanup + (kill-local-variable 'add-node-modules-path-debug) + (kill-local-variable 'add-node-modules-path-command)) + +(ert-deftest add-node-modules-path-single-command-test () + (add-node-modules-path/exec-add-node-modules-path-test '("echo \"/usr\"") '("/usr"))) + +(ert-deftest add-node-modules-path-multiple-commands-test () + (add-node-modules-path/exec-add-node-modules-path-test '("echo \"/etc\"" "echo \"/var\"") '("/etc" "/var"))) + +(ert-deftest add-node-modules-path-multiple-commands-with-failures-test () + (add-node-modules-path/exec-add-node-modules-path-test '("ls -al" "echo \"/var\"" "date" "echo \"/etc\"" "clear") '("/var" "/etc")))