|
| 1 | +;;; cider-cljs.el --- ClojureScript REPL creation -*- lexical-binding: t -*- |
| 2 | + |
| 3 | +;; Copyright © 2012-2026 Tim King, Phil Hagelberg, Bozhidar Batsov |
| 4 | +;; Copyright © 2013-2026 Bozhidar Batsov, Artur Malabarba and CIDER contributors |
| 5 | +;; |
| 6 | +;; Author: Tim King <kingtim@gmail.com> |
| 7 | +;; Phil Hagelberg <technomancy@gmail.com> |
| 8 | +;; Bozhidar Batsov <bozhidar@batsov.dev> |
| 9 | +;; Artur Malabarba <bruce.connor.am@gmail.com> |
| 10 | +;; Hugo Duncan <hugo@hugoduncan.org> |
| 11 | +;; Steve Purcell <steve@sanityinc.com> |
| 12 | +;; Maintainer: Bozhidar Batsov <bozhidar@batsov.dev> |
| 13 | + |
| 14 | +;; This program is free software: you can redistribute it and/or modify |
| 15 | +;; it under the terms of the GNU General Public License as published by |
| 16 | +;; the Free Software Foundation, either version 3 of the License, or |
| 17 | +;; (at your option) any later version. |
| 18 | + |
| 19 | +;; This program is distributed in the hope that it will be useful, |
| 20 | +;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 21 | +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 22 | +;; GNU General Public License for more details. |
| 23 | + |
| 24 | +;; You should have received a copy of the GNU General Public License |
| 25 | +;; along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 26 | + |
| 27 | +;; This file is not part of GNU Emacs. |
| 28 | + |
| 29 | +;;; Commentary: |
| 30 | + |
| 31 | +;; ClojureScript REPL types, requirement checks, init-form builders and the |
| 32 | +;; `cider-register-cljs-repl-type' registry that drives `cider-jack-in-cljs' |
| 33 | +;; and friends. Bundles support for figwheel, figwheel-main, shadow-cljs, |
| 34 | +;; node, weasel, krell, nbb and user-defined custom REPLs. |
| 35 | + |
| 36 | +;;; Code: |
| 37 | + |
| 38 | +(require 'seq) |
| 39 | +(require 'subr-x) |
| 40 | + |
| 41 | +(require 'parseedn) |
| 42 | +(require 'clojure-mode) |
| 43 | + |
| 44 | +(require 'cider-client) |
| 45 | +(require 'nrepl-dict) |
| 46 | + |
| 47 | +(defcustom cider-check-cljs-repl-requirements t |
| 48 | + "When non-nil will run the requirement checks for the different cljs repls. |
| 49 | +Generally you should not disable this unless you run into some faulty check." |
| 50 | + :type 'boolean |
| 51 | + :group 'cider |
| 52 | + :safe #'booleanp |
| 53 | + :package-version '(cider . "0.17.0")) |
| 54 | + |
| 55 | +(defun cider-clojurescript-present-p () |
| 56 | + "Return non nil when ClojureScript is present." |
| 57 | + (or |
| 58 | + ;; This is nil for example for nbb. |
| 59 | + (cider-library-present-p "cljs.core") |
| 60 | + ;; demunge is not defined currently for normal cljs repls. |
| 61 | + ;; So we end up making the two checks |
| 62 | + (nrepl-dict-get (cider-sync-tooling-eval "cljs.core/demunge") "value"))) |
| 63 | + |
| 64 | +(defun cider-verify-clojurescript-is-present () |
| 65 | + "Check whether ClojureScript is present." |
| 66 | + (unless (cider-clojurescript-present-p) |
| 67 | + (user-error "ClojureScript is not available. See https://docs.cider.mx/cider/basics/clojurescript for details"))) |
| 68 | + |
| 69 | +(defun cider-verify-piggieback-is-present () |
| 70 | + "Check whether the piggieback middleware is present." |
| 71 | + (unless (cider-library-present-p "cider.piggieback") |
| 72 | + (user-error "Piggieback 0.4.x (aka cider/piggieback) is not available. See https://docs.cider.mx/cider/basics/clojurescript for details"))) |
| 73 | + |
| 74 | +(defun cider-check-node-requirements () |
| 75 | + "Check whether we can start a Node ClojureScript REPL." |
| 76 | + (cider-verify-piggieback-is-present) |
| 77 | + (unless (executable-find "node") |
| 78 | + (user-error "Node.js is not present on the exec-path. Make sure you've installed it and your exec-path is properly set"))) |
| 79 | + |
| 80 | +(defun cider-check-figwheel-requirements () |
| 81 | + "Check whether we can start a Figwheel ClojureScript REPL." |
| 82 | + (cider-verify-piggieback-is-present) |
| 83 | + (unless (cider-library-present-p "figwheel-sidecar.repl") |
| 84 | + (user-error "Figwheel-sidecar is not available. Please check https://docs.cider.mx/cider/basics/clojurescript for details"))) |
| 85 | + |
| 86 | +(defun cider-check-figwheel-main-requirements () |
| 87 | + "Check whether we can start a Figwheel ClojureScript REPL." |
| 88 | + (cider-verify-piggieback-is-present) |
| 89 | + (unless (cider-library-present-p "figwheel.main") |
| 90 | + (user-error "Figwheel-main is not available. Please check https://docs.cider.mx/cider/basics/clojurescript for details"))) |
| 91 | + |
| 92 | +(defun cider-check-weasel-requirements () |
| 93 | + "Check whether we can start a Weasel ClojureScript REPL." |
| 94 | + (cider-verify-piggieback-is-present) |
| 95 | + (unless (cider-library-present-p "weasel.repl.server") |
| 96 | + (user-error "Weasel in not available. Please check https://docs.cider.mx/cider/basics/clojurescript/#browser-connected-clojurescript-repl for details"))) |
| 97 | + |
| 98 | +(defun cider-check-krell-requirements () |
| 99 | + "Check whether we can start a Krell ClojureScript REPL." |
| 100 | + (cider-verify-piggieback-is-present) |
| 101 | + (unless (cider-library-present-p "krell.repl") |
| 102 | + (user-error "The Krell ClojureScript REPL is not available. Please check https://github.com/vouch-opensource/krell for details"))) |
| 103 | + |
| 104 | +(defun cider-check-shadow-cljs-requirements () |
| 105 | + "Check whether we can start a shadow-cljs REPL." |
| 106 | + (unless (cider-library-present-p "shadow.cljs.devtools.api") |
| 107 | + (user-error "The shadow-cljs ClojureScript REPL is not available. Please check https://docs.cider.mx/cider/basics/clojurescript for details"))) |
| 108 | + |
| 109 | +(defun cider-normalize-cljs-init-options (options) |
| 110 | + "Normalize the OPTIONS string used for initializing a ClojureScript REPL." |
| 111 | + (if (or (string-prefix-p "{" options) |
| 112 | + (string-prefix-p "(" options) |
| 113 | + (string-prefix-p "[" options) |
| 114 | + (string-prefix-p ":" options) |
| 115 | + (string-prefix-p "\"" options)) |
| 116 | + options |
| 117 | + (concat ":" options))) |
| 118 | + |
| 119 | +(defcustom cider-shadow-watched-builds nil |
| 120 | + "Defines the list of builds `shadow-cljs' should watch." |
| 121 | + :type '(repeat string) |
| 122 | + :group 'cider |
| 123 | + :safe #'listp |
| 124 | + :package-version '(cider . "1.0")) |
| 125 | + |
| 126 | +(defcustom cider-shadow-default-options nil |
| 127 | + "Defines default `shadow-cljs' options." |
| 128 | + :type 'string |
| 129 | + :group 'cider |
| 130 | + :safe (lambda (s) (or (null s) (stringp s))) |
| 131 | + :package-version '(cider . "0.18.0")) |
| 132 | + |
| 133 | +(defun cider--shadow-parse-builds (hash) |
| 134 | + "Parses the build names of a shadow-cljs.edn HASH map. |
| 135 | +The default options of `browser-repl' and `node-repl' are also included." |
| 136 | + (let* ((builds (when (hash-table-p hash) |
| 137 | + (gethash :builds hash))) |
| 138 | + (build-keys (when (hash-table-p builds) |
| 139 | + (hash-table-keys builds)))) |
| 140 | + (append build-keys '(browser-repl node-repl)))) |
| 141 | + |
| 142 | +(defun cider--shadow-get-builds () |
| 143 | + "Extract build names from the shadow-cljs.edn config file in the project root." |
| 144 | + (let ((shadow-edn (concat (clojure-project-dir) "shadow-cljs.edn"))) |
| 145 | + (when (file-readable-p shadow-edn) |
| 146 | + (with-temp-buffer |
| 147 | + (insert-file-contents shadow-edn) |
| 148 | + (condition-case err |
| 149 | + (let ((hash (car (parseedn-read '((shadow/env . identity) |
| 150 | + (env . identity)))))) |
| 151 | + (cider--shadow-parse-builds hash)) |
| 152 | + (error |
| 153 | + (user-error "Found an error while reading %s with message: %s" |
| 154 | + shadow-edn |
| 155 | + (error-message-string err)))))))) |
| 156 | + |
| 157 | +(defun cider-shadow-select-cljs-init-form () |
| 158 | + "Generate the init form for a shadow-cljs select-only REPL. |
| 159 | +We have to prompt the user to select a build, that's why this is a command, |
| 160 | +not just a string." |
| 161 | + (let ((form "(do (require '[shadow.cljs.devtools.api :as shadow]) (shadow/nrepl-select %s))") |
| 162 | + (options (or cider-shadow-default-options |
| 163 | + (completing-read "Select shadow-cljs build: " |
| 164 | + (cider--shadow-get-builds))))) |
| 165 | + (format form (cider-normalize-cljs-init-options options)))) |
| 166 | + |
| 167 | +(defun cider-shadow-cljs-init-form () |
| 168 | + "Generate the init form for a shadow-cljs REPL. |
| 169 | +We have to prompt the user to select a build, that's why |
| 170 | +this is a command, not just a string." |
| 171 | + (let* ((shadow-require "(require '[shadow.cljs.devtools.api :as shadow])") |
| 172 | + |
| 173 | + (default-build (cider-normalize-cljs-init-options |
| 174 | + (or cider-shadow-default-options |
| 175 | + (car cider-shadow-watched-builds) |
| 176 | + (completing-read "Select shadow-cljs build: " |
| 177 | + (cider--shadow-get-builds))))) |
| 178 | + |
| 179 | + (watched-builds (or (mapcar #'cider-normalize-cljs-init-options cider-shadow-watched-builds) |
| 180 | + (list default-build))) |
| 181 | + |
| 182 | + (watched-builds-form (mapconcat (lambda (build) (format "(shadow/watch %s)" build)) |
| 183 | + watched-builds |
| 184 | + " ")) |
| 185 | + ;; form used for user-defined builds |
| 186 | + (user-build-form "(do %s %s (shadow/nrepl-select %s))") |
| 187 | + ;; form used for built-in builds like :browser-repl and :node-repl |
| 188 | + (default-build-form "(do %s (shadow/%s))")) |
| 189 | + (if (member default-build '(":browser-repl" ":node-repl")) |
| 190 | + (format default-build-form shadow-require (string-remove-prefix ":" default-build)) |
| 191 | + (format user-build-form shadow-require watched-builds-form default-build)))) |
| 192 | + |
| 193 | +(defcustom cider-figwheel-main-default-options nil |
| 194 | + "Defines the `figwheel.main/start' options. |
| 195 | +
|
| 196 | +Note that figwheel-main/start can also accept a map of options, refer to |
| 197 | +Figwheel for details." |
| 198 | + :type 'string |
| 199 | + :group 'cider |
| 200 | + :safe (lambda (s) (or (null s) (stringp s))) |
| 201 | + :package-version '(cider . "0.18.0")) |
| 202 | + |
| 203 | +(defun cider--figwheel-main-get-builds () |
| 204 | + "Extract build names from the <build-id>.cljs.edn config files. |
| 205 | +Fetches them in the project root." |
| 206 | + (when-let ((project-dir (clojure-project-dir))) |
| 207 | + (let ((builds (directory-files project-dir nil ".*\\.cljs\\.edn"))) |
| 208 | + (mapcar (lambda (f) (string-match "^\\(.*\\)\\.cljs\\.edn" f) |
| 209 | + (match-string 1 f)) |
| 210 | + builds)))) |
| 211 | + |
| 212 | +(defun cider-figwheel-main-init-form () |
| 213 | + "Produce the figwheel-main ClojureScript init form." |
| 214 | + (let ((form "(do (require 'figwheel.main) (figwheel.main/start %s))") |
| 215 | + (builds (cider--figwheel-main-get-builds))) |
| 216 | + (cond |
| 217 | + (cider-figwheel-main-default-options |
| 218 | + (format form (cider-normalize-cljs-init-options (string-trim cider-figwheel-main-default-options)))) |
| 219 | + |
| 220 | + (builds |
| 221 | + (format form (cider-normalize-cljs-init-options (completing-read "Select figwheel-main build: " builds)))) |
| 222 | + |
| 223 | + (t (user-error "No figwheel-main build files (<build-id>.cljs.edn) were found"))))) |
| 224 | + |
| 225 | +(defcustom cider-custom-cljs-repl-init-form nil |
| 226 | + "The form used to start a custom ClojureScript REPL. |
| 227 | +When set it becomes the return value of the `cider-custom-cljs-repl-init-form' |
| 228 | +function, which normally prompts for the init form. |
| 229 | +
|
| 230 | +This defcustom is mostly intended for use with .dir-locals.el for |
| 231 | +cases where it doesn't make sense to register a new ClojureScript REPL type." |
| 232 | + :type 'string |
| 233 | + :group 'cider |
| 234 | + :safe (lambda (s) (or (null s) (stringp s))) |
| 235 | + :package-version '(cider . "0.23.0")) |
| 236 | + |
| 237 | +(defun cider-custom-cljs-repl-init-form () |
| 238 | + "The form used to start a custom ClojureScript REPL. |
| 239 | +Defaults to the value of `cider-custom-cljs-repl-init-form'. |
| 240 | +If it's nil the function will prompt for a form. |
| 241 | +The supplied string will be wrapped in a do form if needed." |
| 242 | + (or |
| 243 | + cider-custom-cljs-repl-init-form |
| 244 | + (let ((form (read-from-minibuffer "Please, provide a form to start a ClojureScript REPL: "))) |
| 245 | + ;; TODO: We should probably make this more robust (e.g. by using a regexp or |
| 246 | + ;; parsing the form). |
| 247 | + (if (string-prefix-p "(do" form) |
| 248 | + form |
| 249 | + (format "(do %s)" form))))) |
| 250 | + |
| 251 | +(defvar cider-cljs-repl-types |
| 252 | + '((figwheel "(do (require 'figwheel-sidecar.repl-api) (figwheel-sidecar.repl-api/start-figwheel!) (figwheel-sidecar.repl-api/cljs-repl))" |
| 253 | + cider-check-figwheel-requirements) |
| 254 | + (figwheel-main cider-figwheel-main-init-form cider-check-figwheel-main-requirements) |
| 255 | + (figwheel-connected "(figwheel-sidecar.repl-api/cljs-repl)" |
| 256 | + cider-check-figwheel-requirements) |
| 257 | + (browser "(do (require 'cljs.repl.browser) (cider.piggieback/cljs-repl (cljs.repl.browser/repl-env)))") |
| 258 | + (node "(do (require 'cljs.repl.node) (cider.piggieback/cljs-repl (cljs.repl.node/repl-env)))" |
| 259 | + cider-check-node-requirements) |
| 260 | + (weasel "(do (require 'weasel.repl.websocket) (cider.piggieback/cljs-repl (weasel.repl.websocket/repl-env :ip \"127.0.0.1\" :port 9001)))" |
| 261 | + cider-check-weasel-requirements) |
| 262 | + (shadow cider-shadow-cljs-init-form cider-check-shadow-cljs-requirements) |
| 263 | + (shadow-select cider-shadow-select-cljs-init-form cider-check-shadow-cljs-requirements) |
| 264 | + (krell "(require '[clojure.edn :as edn] |
| 265 | + '[clojure.java.io :as io] |
| 266 | + '[cider.piggieback] |
| 267 | + '[krell.api :as krell] |
| 268 | + '[krell.repl]) |
| 269 | +(def config (edn/read-string (slurp (io/file \"build.edn\")))) |
| 270 | +(apply cider.piggieback/cljs-repl (krell.repl/repl-env) (mapcat identity config))" |
| 271 | + cider-check-krell-requirements) |
| 272 | + ;; native cljs repl, no form required. |
| 273 | + (nbb) |
| 274 | + (custom cider-custom-cljs-repl-init-form nil)) |
| 275 | + "A list of supported ClojureScript REPLs. |
| 276 | +
|
| 277 | +For each one we have its name, and then, if the repl is not a native |
| 278 | +ClojureScript REPL, the form we need to evaluate in a Clojure REPL to |
| 279 | +switch to the ClojureScript REPL and functions to verify their |
| 280 | +requirements. |
| 281 | +
|
| 282 | +The form, if any, should be either a string or a function producing a |
| 283 | +string.") |
| 284 | + |
| 285 | +(defun cider-register-cljs-repl-type (type &optional init-form requirements-fn) |
| 286 | + "Register a new ClojureScript REPL type. |
| 287 | +
|
| 288 | +Types are defined by the following: |
| 289 | +
|
| 290 | +- TYPE - symbol identifier that will be used to refer to the REPL type |
| 291 | +- INIT-FORM - (optional) string or function (symbol) producing string |
| 292 | +- REQUIREMENTS-FN - function to check whether the REPL can be started. |
| 293 | +This param is optional. |
| 294 | +
|
| 295 | +All this function does is modifying `cider-cljs-repl-types'. |
| 296 | +It's intended to be used in your Emacs config." |
| 297 | + (unless (symbolp type) |
| 298 | + (user-error "The REPL type must be a symbol")) |
| 299 | + (unless (or (null init-form) (stringp init-form) (symbolp init-form)) |
| 300 | + (user-error "The init form must be a string or a symbol referring to a function or nil")) |
| 301 | + (unless (or (null requirements-fn) (symbolp requirements-fn)) |
| 302 | + (user-error "The requirements-fn must be a symbol referring to a function")) |
| 303 | + (add-to-list 'cider-cljs-repl-types (list type init-form requirements-fn))) |
| 304 | + |
| 305 | +(defcustom cider-default-cljs-repl nil |
| 306 | + "The default ClojureScript REPL to start. |
| 307 | +This affects commands like `cider-jack-in-cljs'. Generally it's |
| 308 | +intended to be set via .dir-locals.el for individual projects, as it's |
| 309 | +relatively unlikely you'd like to use the same type of REPL in each project |
| 310 | +you're working on." |
| 311 | + :type '(choice (const :tag "Figwheel" figwheel) |
| 312 | + (const :tag "Figwheel Main" figwheel-main) |
| 313 | + (const :tag "Browser" browser) |
| 314 | + (const :tag "Node" node) |
| 315 | + (const :tag "Weasel" weasel) |
| 316 | + (const :tag "Shadow" shadow) |
| 317 | + (const :tag "Shadow w/o Server" shadow-select) |
| 318 | + (const :tag "Krell" krell) |
| 319 | + (const :tag "Nbb" nbb) |
| 320 | + (const :tag "Basilisp" basilisp) |
| 321 | + (const :tag "Custom" custom)) |
| 322 | + :group 'cider |
| 323 | + :safe #'symbolp |
| 324 | + :package-version '(cider . "0.17.0")) |
| 325 | + |
| 326 | +(defvar cider--select-cljs-repl-history nil) |
| 327 | +(defun cider-select-cljs-repl (&optional default) |
| 328 | + "Select the ClojureScript REPL to use with `cider-jack-in-cljs'. |
| 329 | +DEFAULT is the default ClojureScript REPL to offer in completion." |
| 330 | + (let ((repl-types (mapcar #'car cider-cljs-repl-types))) |
| 331 | + (intern (completing-read "Select ClojureScript REPL type: " repl-types |
| 332 | + nil nil nil 'cider--select-cljs-repl-history |
| 333 | + (or default (car cider--select-cljs-repl-history)))))) |
| 334 | + |
| 335 | +(defun cider-cljs-repl-form (repl-type) |
| 336 | + "Get the cljs REPL form for REPL-TYPE, if any." |
| 337 | + (if-let* ((repl-type-info (seq-find |
| 338 | + (lambda (entry) |
| 339 | + (eq (car entry) repl-type)) |
| 340 | + cider-cljs-repl-types))) |
| 341 | + (when-let ((repl-form (cadr repl-type-info))) |
| 342 | + ;; repl-form can be either a string or a function producing a string |
| 343 | + (if (symbolp repl-form) |
| 344 | + (funcall repl-form) |
| 345 | + repl-form)) |
| 346 | + (user-error "No ClojureScript REPL type %s found. Please make sure that `cider-cljs-repl-types' has an entry for it" repl-type))) |
| 347 | + |
| 348 | +(defun cider-verify-cljs-repl-requirements (&optional repl-type) |
| 349 | + "Verify that the requirements for REPL-TYPE are met. |
| 350 | +Return REPL-TYPE if requirements are met." |
| 351 | + (let ((repl-type (or repl-type |
| 352 | + cider-default-cljs-repl |
| 353 | + (cider-select-cljs-repl)))) |
| 354 | + (when cider-check-cljs-repl-requirements |
| 355 | + (when-let* ((fun (nth 2 (seq-find |
| 356 | + (lambda (entry) |
| 357 | + (eq (car entry) repl-type)) |
| 358 | + cider-cljs-repl-types)))) |
| 359 | + (funcall fun))) |
| 360 | + repl-type)) |
| 361 | + |
| 362 | +(defun cider--check-cljs (&optional cljs-type no-error) |
| 363 | + "Verify that all cljs requirements are met for CLJS-TYPE connection. |
| 364 | +Return REPL-TYPE of requirement are met, and throw an ‘user-error’ otherwise. |
| 365 | +When NO-ERROR is non-nil, don't throw an error, issue a message and return |
| 366 | +nil." |
| 367 | + (if no-error |
| 368 | + (condition-case ex |
| 369 | + (progn |
| 370 | + (cider-verify-clojurescript-is-present) |
| 371 | + (cider-verify-cljs-repl-requirements cljs-type)) |
| 372 | + (error |
| 373 | + (message "Invalid ClojureScript dependency: %S" ex) |
| 374 | + nil)) |
| 375 | + (cider-verify-clojurescript-is-present) |
| 376 | + (cider-verify-cljs-repl-requirements cljs-type))) |
| 377 | + |
| 378 | +(defun cider--offer-to-open-app-in-browser (server-buf) |
| 379 | + "Look for a server address in SERVER-BUF and offer to open it." |
| 380 | + (when (buffer-live-p server-buf) |
| 381 | + (with-current-buffer server-buf |
| 382 | + (save-excursion |
| 383 | + (goto-char (point-min)) |
| 384 | + (when-let* ((url (and (search-forward-regexp "http://localhost:[0-9]+" nil 'noerror) |
| 385 | + (match-string 0)))) |
| 386 | + (when (y-or-n-p (format "Visit ‘%s’ in a browser? " url)) |
| 387 | + (browse-url url))))))) |
| 388 | + |
| 389 | +(provide 'cider-cljs) |
| 390 | + |
| 391 | +;;; cider-cljs.el ends here |
0 commit comments