You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env bash
  2. declare -A aliases
  3. declare -A helptext
  4. err() { printf '%s\n' "$@" >&2; return 1; }
  5. die() {
  6. (( $# > 0 )) && err "$@"
  7. exit 1
  8. }
  9. select_from() {
  10. local cmd='command -v'
  11. for a; do
  12. case "$a" in
  13. -c) cmd="$2"; shift 2 ;;
  14. esac
  15. done
  16. for c; do
  17. if $cmd "${c%% *}" &> /dev/null; then
  18. echo "$c"
  19. return 0
  20. fi
  21. done
  22. return 1
  23. }
  24. has() {
  25. local v c
  26. if [[ $1 = '-v' ]]; then
  27. v=1
  28. shift
  29. fi
  30. for c; do c="${c%% *}"
  31. if ! command -v "$c" &> /dev/null; then
  32. (( v > 0 )) && err "$c not found"
  33. return 1
  34. fi
  35. done
  36. }
  37. export -f has
  38. aliases[s]=search
  39. helptext['search']="search and install packages"
  40. subcmd_search() {
  41. local init
  42. if [[ -n $1 ]]; then init=$(npm --json search "$*" | pretty_npm_search); fi
  43. # SHELL is needed to use exported functions if default shell is not bash
  44. SHELL=$(which bash) fzf \
  45. --inline-info \
  46. --query="$*" \
  47. --phony \
  48. --multi \
  49. --preview-window=hidden \
  50. --header='enter to install, C-d saves as devDependency, C-v to pick versions' \
  51. --bind='?:toggle-preview' \
  52. --bind='change:reload:npm --json search {q} | pretty_npm_search' \
  53. --bind='ctrl-v:execute:subcmd_ls-versions {1} <> /dev/tty' \
  54. --bind="enter:execute:subcmd_install {+1} <> /dev/tty" \
  55. --bind="ctrl-d:execute:subcmd_install -D {+1} <> /dev/tty" \
  56. --preview="npm view {1}" \
  57. <<< "$init"
  58. }
  59. aliases[lsv]='ls-versions'
  60. helptext['ls-versions']='list and install versions'
  61. subcmd_ls-versions() {
  62. local package="$1"
  63. fzf --tac --preview="npm view ${package}@{1}" \
  64. --header="choose version for $package | C-d saves as devDependency" \
  65. --bind="enter:execute:subcmd_install '${package}@{1}' <> /dev/tty" \
  66. --bind="ctrl-d:execute:subcmd_install -D '${package}@{1}' <> /dev/tty" \
  67. < <(npm --json view "$package" | jq -r '.versions[]') # label with .dist-tags?)
  68. }
  69. export -f subcmd_ls-versions
  70. aliases[un]=uninstall
  71. helptext[uninstall]='uninstall packages'
  72. subcmd_uninstall() {
  73. local rm
  74. mapfile -t rm < <(
  75. jq -r '{dependencies, devDependencies}[] | keys[]' package.json |
  76. fzf -m --query="$*")
  77. (( ${#rm} > 0 )) || return
  78. $(select_from 'yarn remove' 'npm uninstall') "${rm[@]}"
  79. }
  80. aliases[i]=install
  81. helptext[install]='install packages'
  82. subcmd_install() {
  83. if [[ -e yarn.lock ]] && has yarn; then
  84. yarn add "$@"
  85. else
  86. npm i -S "$@"
  87. fi
  88. }
  89. export -f subcmd_install
  90. helptext[init]='guided project setup'
  91. subcmd_init() {
  92. # shellcheck disable=2091
  93. $(select_from 'git-flow init -d' 'git init') > /dev/null
  94. npm init -y > /dev/null
  95. [[ -e .gitignore ]] || curl -sL https://raw.githubusercontent.com/toptal/gitignore/master/templates/Node.gitignore > .gitignore
  96. # install typescript? eslint? prettier? husky + lint-staged?
  97. # react? vue? bundlers etc
  98. }
  99. helptext[lint]='setup or use eslint'
  100. subcmd_lint() { # TODO
  101. # should check if typescript, react, babel is installed, use relevant plugins
  102. if [[ $(jq '.devDependencies.eslint' package.json) = null ]]; then
  103. npx eslint --init
  104. fi
  105. npx eslint --ignore-path=.gitignore "$@"
  106. }
  107. aliases[fmt]=format
  108. helptext[format]='setup or use prettier'
  109. subcmd_format() { # TODO
  110. if [[ $(jq '.devDependencies.prettier' package.json) = null ]]; then
  111. subcmd_install -D prettier # eslint-{config,plugin}-prettier
  112. fi
  113. npx prettier --ignore-path .gitignore --write "$@"
  114. }
  115. aliases[-h]=help
  116. helptext[help]='show this help '
  117. subcmd_help() {
  118. LESS=-FEXR less <<-HELP
  119. js [subcmd] [options]
  120. $(for c in "${subcmds_avail[@]}"; do
  121. printf " %s\n %s\n" "$c" "${helptext[$c]}"
  122. done)
  123. HELP
  124. }
  125. pretty_npm_search() { jq -r '.[] | "\(.name) \(.version)\t\(.description[0:80])\t\(.author.name // .publisher.username)\t\((.keywords // []) | join(" "))"' | column -t -s $'\t'; }
  126. export -f pretty_npm_search
  127. has -v fzf jq npm || die
  128. mapfile -t subcmds_avail < <(compgen -A function | awk '/^subcmd_/ { sub(/^subcmd_/, "", $0); print }')
  129. if (( $# < 1 )); then
  130. err 'missing command'
  131. subcmd_help
  132. exit 1
  133. elif has "subcmd_$1"; then
  134. subcmd="subcmd_$1"
  135. shift
  136. "$subcmd" "$@"
  137. elif [[ -v aliases[$1] ]]; then
  138. subcmd=subcmd_${aliases[$1]}
  139. shift
  140. "$subcmd" "$@"
  141. else
  142. err 'unknown command'
  143. subcmd_help
  144. exit 1
  145. fi