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.
|
|
;;; skewer-bower.el --- dynamic library loading -*- lexical-binding: t; -*-
;; This is free and unencumbered software released into the public domain.
;;; Commentary:
;; This package loads libraries into the current page using the bower;; infrastructure. Note: bower is not actually used by this package;; and so does *not* need to be installed. Only git is required (see;; `skewer-bower-git-executable'). It will try to learn how to run git;; from Magit if available.
;; The interactive command for loading libraries is;; `skewer-bower-load'. It will prompt for a library and a version,;; automatically fetching it from the bower infrastructure if needed.;; For example, I often find it handy to load some version of jQuery;; when poking around at a page that doesn't already have it loaded.
;; Caveat: unfortunately the bower infrastructure is a mess; many;; packages are in some sort of broken state -- missing dependencies,;; missing metadata, broken metadata, or an invalid repository URL.;; Some of this is due to under-specification of the metadata by the;; bower project. Broken packages are unlikely to be loadable by;; skewer-bower.
;;; Code:
(require 'cl-lib)(require 'skewer-mode)(require 'simple-httpd)(require 'magit nil t) ; optional
(defcustom skewer-bower-cache-dir (locate-user-emacs-file "skewer-cache") "Location of library cache (git repositories)." :type 'string :group 'skewer)
(defcustom skewer-bower-endpoint "https://bower.herokuapp.com" "Endpoint for accessing package information." :type 'string :group 'skewer)
(defcustom skewer-bower-json '("bower.json" "package.json" "component.json") "Files to search for package metadata." :type 'list :group 'skewer)
; Try to match Magit's configuration if available(defcustom skewer-bower-git-executable "git" "Name of the git executable." :type 'string :group 'skewer)
(defvar skewer-bower-packages nil "Alist of all packages known to bower.")
(defvar skewer-bower-refreshed nil "List of packages that have been refreshed recently. This keeps
them from hitting the network frequently.")
;;;###autoload(defun skewer-bower-refresh () "Update the package listing and packages synchronously." (interactive) (cl-declare (special url-http-end-of-headers)) (setf skewer-bower-refreshed nil) (with-current-buffer (url-retrieve-synchronously (concat skewer-bower-endpoint "/packages")) (setf (point) url-http-end-of-headers) (setf skewer-bower-packages (cl-sort (cl-loop for package across (json-read) collect (cons (cdr (assoc 'name package)) (cdr (assoc 'url package)))) #'string< :key #'car))))
;; Git functions
(defun skewer-bower-cache (package) "Return the cache repository directory for PACKAGE." (unless (file-exists-p skewer-bower-cache-dir) (make-directory skewer-bower-cache-dir t)) (expand-file-name package skewer-bower-cache-dir))
(defun skewer-bower-git (package &rest args) "Run git for PACKAGE's repository with ARGS." (with-temp-buffer (when (zerop (apply #'call-process skewer-bower-git-executable nil t nil (format "--git-dir=%s" (skewer-bower-cache package)) args)) (buffer-string))))
(defun skewer-bower-git-clone (url package) "Clone or fetch PACKAGE's repository from URL if needed." (if (member package skewer-bower-refreshed) t (let* ((cache (skewer-bower-cache package)) (status (if (file-exists-p cache) (when (skewer-bower-git package "fetch") (push package skewer-bower-refreshed)) (skewer-bower-git package "clone" "--bare" url cache)))) (not (null status)))))
(defun skewer-bower-git-show (package version file) "Grab FILE from PACKAGE at version VERSION." (when (string-match-p "^\\./" file) ; avoid relative paths (setf file (substring file 2))) (skewer-bower-git package "show" (format "%s:%s" version file)))
(defun skewer-bower-git-tag (package) "List all the tags in PACKAGE's repository." (split-string (skewer-bower-git package "tag")))
;; Bower functions
(defun skewer-bower-package-ensure (package) "Ensure a package is installed in the cache and up to date.
Emit an error if the package could not be ensured."
(when (null skewer-bower-packages) (skewer-bower-refresh)) (let ((url (cdr (assoc package skewer-bower-packages)))) (when (null url) (error "Unknown package: %s" package)) (when (null (skewer-bower-git-clone url package)) (error "Failed to fetch: %s" url)) t))
(defun skewer-bower-package-versions (package) "List the available versions for a package. Always returns at
least one version."
(skewer-bower-package-ensure package) (or (sort (skewer-bower-git-tag package) #'string<) (list "master")))
(defun skewer-bower-get-config (package &optional version) "Get the configuration alist for PACKAGE at VERSION. Return nil
if no configuration could be found."
(skewer-bower-package-ensure package) (unless version (setf version "master")) (json-read-from-string (cl-loop for file in skewer-bower-json for config = (skewer-bower-git-show package version file) when config return it finally (return "null"))))
;; Serving the library
(defvar skewer-bower-history () "Library selection history for `completing-read'.")
(defun skewer-bowser--path (package version main) "Return the simple-httpd hosted path for PACKAGE." (format "/skewer/bower/%s/%s/%s" package (or version "master") main))
(defun skewer-bower-prompt-package () "Prompt for a package and version from the user." (when (null skewer-bower-packages) (skewer-bower-refresh)) ;; ido-completing-read bug workaround: (when (> (length skewer-bower-history) 32) (setf skewer-bower-history (cl-subseq skewer-bower-history 0 16))) (let* ((packages (mapcar #'car skewer-bower-packages)) (selection (nconc skewer-bower-history packages)) (package (completing-read "Library: " selection nil t nil 'skewer-bower-history)) (versions (reverse (skewer-bower-package-versions package))) (version (completing-read "Version: " versions nil t nil nil (car versions)))) (list package version)))
(defun skewer-bower--js-p (filename) "Return non-nil if FILENAME looks like JavaScript." (string-match "\\.js$" filename))
(defun skewer-bower-guess-main (package version config) "Attempt to determine the main entrypoints from a potentially
incomplete or incorrect bower configuration. Returns nil ifguessing failed."
(let ((check (apply-partially #'skewer-bower-git-show package version)) (main (cdr (assoc 'main config)))) (cond ((and (vectorp main) (cl-some check main)) (cl-coerce (cl-remove-if-not #'skewer-bower--js-p main) 'list)) ((and (stringp main) (funcall check main)) (list main)) ((funcall check (concat package ".js")) (list (concat package ".js"))) ((funcall check package) (list package)))))
;;;###autoload(defun skewer-bower-load (package &optional version) "Dynamically load a library from bower into the current page." (interactive (skewer-bower-prompt-package)) (let* ((config (skewer-bower-get-config package version)) (deps (cdr (assoc 'dependencies config))) (main (skewer-bower-guess-main package version config))) (when (null main) (error "Could not load %s (%s): no \"main\" entrypoint specified" package version)) (cl-loop for (dep . version) in deps do (skewer-bower-load (format "%s" dep) version)) (cl-loop for entrypoint in main for path = (skewer-bowser--path package version entrypoint) do (skewer-eval path nil :type "script"))))
(defservlet skewer/bower "application/javascript; charset=utf-8" (path) "Serve a script from the local bower repository cache." (cl-destructuring-bind (_ _skewer _bower package version . parts) (split-string path "/") (let* ((file (mapconcat #'identity parts "/")) (contents (skewer-bower-git-show package version file))) (if contents (insert contents) (httpd-error t 404)))))
(provide 'skewer-bower)
;;; skewer-bower.el ends here
|