Skip to content

Commit 5da43e7

Browse files
committed
Extract ClojureScript REPL creation into cider-cljs.el
Move the ~340-line "ClojureScript REPL creation" section out of cider.el and into its own module. The new file holds: - the cljs-repl registry (cider-cljs-repl-types, cider-register-cljs-repl-type, cider-default-cljs-repl, cider-select-cljs-repl) - per-flavor helpers and init-form builders (cider-shadow-cljs-init-form, cider-figwheel-main-init-form, cider-shadow-select-cljs-init-form, cider-custom-cljs-repl-init-form, the supporting cider--shadow-* / cider--figwheel-main-* utilities) - requirement checks (cider-check-figwheel-requirements, cider-check-shadow-cljs-requirements, etc., plus the ClojureScript/Piggieback presence checks) - the cljs-repl dispatch entry points cider-cljs-repl-form, cider-verify-cljs-repl-requirements, cider--check-cljs - cider--offer-to-open-app-in-browser, used by the cljs jack-in flow The cider-register-cljs-repl-type registry was already a clean seam: the few callers from cider.el (cider--check-cljs, cider-cljs-repl-form, cider--offer-to-open-app-in-browser) still reach the new file via require. cider-cljs.el itself only needs cider-client (for cider-library-present-p, cider-sync-tooling-eval) and nrepl-dict. cider.el drops from 1801 to 1463 lines.
1 parent 3a257e2 commit 5da43e7

2 files changed

Lines changed: 392 additions & 339 deletions

File tree

lisp/cider-cljs.el

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
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

Comments
 (0)