Personal emacs config
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.

217 lines
8.3 KiB

  1. ;;; skewer-bower.el --- dynamic library loading -*- lexical-binding: t; -*-
  2. ;; This is free and unencumbered software released into the public domain.
  3. ;;; Commentary:
  4. ;; This package loads libraries into the current page using the bower
  5. ;; infrastructure. Note: bower is not actually used by this package
  6. ;; and so does *not* need to be installed. Only git is required (see
  7. ;; `skewer-bower-git-executable'). It will try to learn how to run git
  8. ;; from Magit if available.
  9. ;; The interactive command for loading libraries is
  10. ;; `skewer-bower-load'. It will prompt for a library and a version,
  11. ;; automatically fetching it from the bower infrastructure if needed.
  12. ;; For example, I often find it handy to load some version of jQuery
  13. ;; when poking around at a page that doesn't already have it loaded.
  14. ;; Caveat: unfortunately the bower infrastructure is a mess; many
  15. ;; packages are in some sort of broken state -- missing dependencies,
  16. ;; missing metadata, broken metadata, or an invalid repository URL.
  17. ;; Some of this is due to under-specification of the metadata by the
  18. ;; bower project. Broken packages are unlikely to be loadable by
  19. ;; skewer-bower.
  20. ;;; Code:
  21. (require 'cl-lib)
  22. (require 'skewer-mode)
  23. (require 'simple-httpd)
  24. (require 'magit nil t) ; optional
  25. (defcustom skewer-bower-cache-dir (locate-user-emacs-file "skewer-cache")
  26. "Location of library cache (git repositories)."
  27. :type 'string
  28. :group 'skewer)
  29. (defcustom skewer-bower-endpoint "https://bower.herokuapp.com"
  30. "Endpoint for accessing package information."
  31. :type 'string
  32. :group 'skewer)
  33. (defcustom skewer-bower-json '("bower.json" "package.json" "component.json")
  34. "Files to search for package metadata."
  35. :type 'list
  36. :group 'skewer)
  37. ; Try to match Magit's configuration if available
  38. (defcustom skewer-bower-git-executable "git"
  39. "Name of the git executable."
  40. :type 'string
  41. :group 'skewer)
  42. (defvar skewer-bower-packages nil
  43. "Alist of all packages known to bower.")
  44. (defvar skewer-bower-refreshed nil
  45. "List of packages that have been refreshed recently. This keeps
  46. them from hitting the network frequently.")
  47. ;;;###autoload
  48. (defun skewer-bower-refresh ()
  49. "Update the package listing and packages synchronously."
  50. (interactive)
  51. (cl-declare (special url-http-end-of-headers))
  52. (setf skewer-bower-refreshed nil)
  53. (with-current-buffer
  54. (url-retrieve-synchronously (concat skewer-bower-endpoint "/packages"))
  55. (setf (point) url-http-end-of-headers)
  56. (setf skewer-bower-packages
  57. (cl-sort
  58. (cl-loop for package across (json-read)
  59. collect (cons (cdr (assoc 'name package))
  60. (cdr (assoc 'url package))))
  61. #'string< :key #'car))))
  62. ;; Git functions
  63. (defun skewer-bower-cache (package)
  64. "Return the cache repository directory for PACKAGE."
  65. (unless (file-exists-p skewer-bower-cache-dir)
  66. (make-directory skewer-bower-cache-dir t))
  67. (expand-file-name package skewer-bower-cache-dir))
  68. (defun skewer-bower-git (package &rest args)
  69. "Run git for PACKAGE's repository with ARGS."
  70. (with-temp-buffer
  71. (when (zerop (apply #'call-process skewer-bower-git-executable nil t nil
  72. (format "--git-dir=%s" (skewer-bower-cache package))
  73. args))
  74. (buffer-string))))
  75. (defun skewer-bower-git-clone (url package)
  76. "Clone or fetch PACKAGE's repository from URL if needed."
  77. (if (member package skewer-bower-refreshed)
  78. t
  79. (let* ((cache (skewer-bower-cache package))
  80. (status
  81. (if (file-exists-p cache)
  82. (when (skewer-bower-git package "fetch")
  83. (push package skewer-bower-refreshed))
  84. (skewer-bower-git package "clone" "--bare" url cache))))
  85. (not (null status)))))
  86. (defun skewer-bower-git-show (package version file)
  87. "Grab FILE from PACKAGE at version VERSION."
  88. (when (string-match-p "^\\./" file) ; avoid relative paths
  89. (setf file (substring file 2)))
  90. (skewer-bower-git package "show" (format "%s:%s" version file)))
  91. (defun skewer-bower-git-tag (package)
  92. "List all the tags in PACKAGE's repository."
  93. (split-string (skewer-bower-git package "tag")))
  94. ;; Bower functions
  95. (defun skewer-bower-package-ensure (package)
  96. "Ensure a package is installed in the cache and up to date.
  97. Emit an error if the package could not be ensured."
  98. (when (null skewer-bower-packages) (skewer-bower-refresh))
  99. (let ((url (cdr (assoc package skewer-bower-packages))))
  100. (when (null url)
  101. (error "Unknown package: %s" package))
  102. (when (null (skewer-bower-git-clone url package))
  103. (error "Failed to fetch: %s" url))
  104. t))
  105. (defun skewer-bower-package-versions (package)
  106. "List the available versions for a package. Always returns at
  107. least one version."
  108. (skewer-bower-package-ensure package)
  109. (or (sort (skewer-bower-git-tag package) #'string<)
  110. (list "master")))
  111. (defun skewer-bower-get-config (package &optional version)
  112. "Get the configuration alist for PACKAGE at VERSION. Return nil
  113. if no configuration could be found."
  114. (skewer-bower-package-ensure package)
  115. (unless version (setf version "master"))
  116. (json-read-from-string
  117. (cl-loop for file in skewer-bower-json
  118. for config = (skewer-bower-git-show package version file)
  119. when config return it
  120. finally (return "null"))))
  121. ;; Serving the library
  122. (defvar skewer-bower-history ()
  123. "Library selection history for `completing-read'.")
  124. (defun skewer-bowser--path (package version main)
  125. "Return the simple-httpd hosted path for PACKAGE."
  126. (format "/skewer/bower/%s/%s/%s" package (or version "master") main))
  127. (defun skewer-bower-prompt-package ()
  128. "Prompt for a package and version from the user."
  129. (when (null skewer-bower-packages) (skewer-bower-refresh))
  130. ;; ido-completing-read bug workaround:
  131. (when (> (length skewer-bower-history) 32)
  132. (setf skewer-bower-history (cl-subseq skewer-bower-history 0 16)))
  133. (let* ((packages (mapcar #'car skewer-bower-packages))
  134. (selection (nconc skewer-bower-history packages))
  135. (package (completing-read "Library: " selection nil t nil
  136. 'skewer-bower-history))
  137. (versions (reverse (skewer-bower-package-versions package)))
  138. (version (completing-read "Version: " versions
  139. nil t nil nil (car versions))))
  140. (list package version)))
  141. (defun skewer-bower--js-p (filename)
  142. "Return non-nil if FILENAME looks like JavaScript."
  143. (string-match "\\.js$" filename))
  144. (defun skewer-bower-guess-main (package version config)
  145. "Attempt to determine the main entrypoints from a potentially
  146. incomplete or incorrect bower configuration. Returns nil if
  147. guessing failed."
  148. (let ((check (apply-partially #'skewer-bower-git-show package version))
  149. (main (cdr (assoc 'main config))))
  150. (cond ((and (vectorp main) (cl-some check main))
  151. (cl-coerce (cl-remove-if-not #'skewer-bower--js-p main) 'list))
  152. ((and (stringp main) (funcall check main))
  153. (list main))
  154. ((funcall check (concat package ".js"))
  155. (list (concat package ".js")))
  156. ((funcall check package)
  157. (list package)))))
  158. ;;;###autoload
  159. (defun skewer-bower-load (package &optional version)
  160. "Dynamically load a library from bower into the current page."
  161. (interactive (skewer-bower-prompt-package))
  162. (let* ((config (skewer-bower-get-config package version))
  163. (deps (cdr (assoc 'dependencies config)))
  164. (main (skewer-bower-guess-main package version config)))
  165. (when (null main)
  166. (error "Could not load %s (%s): no \"main\" entrypoint specified"
  167. package version))
  168. (cl-loop for (dep . version) in deps
  169. do (skewer-bower-load (format "%s" dep) version))
  170. (cl-loop for entrypoint in main
  171. for path = (skewer-bowser--path package version entrypoint)
  172. do (skewer-eval path nil :type "script"))))
  173. (defservlet skewer/bower "application/javascript; charset=utf-8" (path)
  174. "Serve a script from the local bower repository cache."
  175. (cl-destructuring-bind (_ _skewer _bower package version . parts)
  176. (split-string path "/")
  177. (let* ((file (mapconcat #'identity parts "/"))
  178. (contents (skewer-bower-git-show package version file)))
  179. (if contents
  180. (insert contents)
  181. (httpd-error t 404)))))
  182. (provide 'skewer-bower)
  183. ;;; skewer-bower.el ends here