├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── boot.properties ├── build.boot ├── src └── powerlaces │ ├── boot_figreload.clj │ └── boot_figreload │ ├── figwheel.clj │ ├── messages.clj │ ├── server.clj │ └── util.clj ├── test.sh └── test └── powerlaces └── boot_figreload └── util_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | ### BOOT ####################################################################### 2 | 3 | /.boot/ 4 | 5 | ### LEININGEN ################################################################## 6 | 7 | .lein-* 8 | 9 | ### MAVEN (include pom.xml in /base) ########################################### 10 | 11 | *.jar 12 | *.war 13 | pom.xml 14 | pom.xml.asc 15 | 16 | ### NREPL ###################################################################### 17 | 18 | .repl-* 19 | .nrepl-* 20 | 21 | ### JAVA ####################################################################### 22 | 23 | /hs_err_pid*.log 24 | 25 | ### OSX ######################################################################## 26 | 27 | .DS_Store 28 | 29 | ### EMACS ###################################################################### 30 | 31 | [#]*[#] 32 | .dir-locals.el 33 | 34 | ### VIM ######################################################################## 35 | 36 | *.swn 37 | *.swo 38 | *.swp 39 | 40 | ### PROJECT #################################################################### 41 | 42 | /zzz/ 43 | /target/ 44 | 45 | ### TAGS #################################################################### 46 | 47 | GPATH 48 | GRTAGS 49 | GSYMS 50 | GTAGS 51 | TAGS 52 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | script: ./test.sh 4 | install: 5 | - mkdir -p ~/bin 6 | - export PATH=~/bin:$PATH 7 | - curl -L https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh -o ~/bin/boot 8 | - chmod +x ~/bin/boot 9 | jdk: 10 | - openjdk7 11 | - oraclejdk8 12 | cache: 13 | directories: 14 | - $HOME/.m2 15 | - $HOME/.boot/cache 16 | - $HOME/bin 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.5.15 - to be released 2 | 3 | - New release aligned with Figwheel `0.5.15`. 4 | 5 | **[compare](https://github.com/boot-clj/boot-fig reload/compare/0.5.14...master)** 6 | 7 | ### 0.5.14 8 | 9 | - New release aligned with Figwheel `0.5.14`. 10 | 11 | **[compare](https://github.com/boot-clj/boot-fig reload/compare/0.5.13...0.5.14)** 12 | 13 | ### [0.5.13](https://github.com/boot-clj/boot-figreload/releases/tag/0.5.13) 14 | 15 | - New release aligned with Figwheel `0.5.13`. 16 | 17 | **[compare](https://github.com/boot-clj/boot-fig reload/compare/0.5.9...0.5.13)** 18 | 19 | ## [0.5.9](https://github.com/boot-clj/boot-figreload/tag/0.5.9) 20 | 21 | #### First release matching lein-figwheel's version 22 | 23 | Handled server to client messages: 24 | 25 | - [x] `:files-changed` 26 | - [x] `:compile-warning` 27 | - [x] `:compile-failed` 28 | - [x] `:css-files-changed` 29 | 30 | Implemented [client to server](https://github.com/arichiardi/lein-figwheel/blob/boot-reload-changes/sidecar/src/figwheel_sidecar/components/figwheel_server.clj#L75) messages: 31 | 32 | - [ ] `"file-selected"` 33 | - [ ] `"callback"` 34 | 35 | Misc tasks: 36 | 37 | - [x] Inject the Figwheel bootstrap script 38 | - [x] Handle individual `js-onload` per build id (untested but there) 39 | - [x] Figwheel version 40 | - [x] Use Figwheel [init code](https://github.com/bhauman/lein-figwheel/blob/cc2d188ab041fc92551d3c4a8201729c47fe5846/sidecar/src/figwheel_sidecar/build_middleware/injection.clj#L171) (?) 41 | - [ ] Handle `boot-reload`'s `:asset-host` in Figwheel ([link to comments](https://github.com/adzerk-oss/boot-reload/commit/e27e330d9f688875ba19d56e825cd9e81013e58e#commitcomment-20350456)) 42 | - [ ] Pass the right `:open-file` option to Figwheel 43 | - [ ] Solve the "first message lost" problem with a message queue (?) 44 | - [x] Assert needed dependencies 45 | - [ ] Repl integration (at the moment supported via `boot-cljs-repl`) 46 | 47 | To be thorougly tested: 48 | 49 | - [x] Node client 50 | - [ ] Web-worker client 51 | - [ ] Trigger of multiple `js-onload`s 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Contributions are welcome. 4 | 5 | Please file bug reports and feature requests to https://github.com/adzerk-oss/boot-reload/issues. 6 | 7 | ## Making changes 8 | 9 | * Fork the repository on Github 10 | * Create a topic branch from where you want to base your work (usually the master branch) 11 | * Check the formatting rules from existing code (no trailing whitepace, mostly default indentation) 12 | * Ensure any new code is well-tested, and if possible, any issue fixed is covered by one or more new tests 13 | * Push your code to your fork of the repository 14 | * Make a Pull Request 15 | 16 | ## Commit messages 17 | 18 | 1. Separate subject from body with a blank line 19 | 2. Limit the subject line to 50 characters 20 | 3. Capitalize the subject line 21 | 4. Do not end the subject line with a period 22 | 5. Use the imperative mood in the subject line 23 | - "Add x", "Fix y", "Support z", "Remove x" 24 | 6. Wrap the body at 72 characters 25 | 7. Use the body to explain what and why vs. how 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boot-figreload 2 | [![Clojars Project](https://img.shields.io/clojars/v/powerlaces/boot-figreload.svg)](https://clojars.org/powerlaces/boot-figreload) 3 | 4 | [Boot][1] task to automatically reload resources in the browser when files in 5 | the project change. Featuring [lein-figwheel][2]. 6 | 7 | * Provides the `reload` task 8 | * Reload client can show warnings and exceptions from ClojureScript build on **heads-up display**. 9 | * Requires `adzerk/boot-cljs` >= `2.0.0` 10 | 11 | ## Usage 12 | 13 | Add dependency to `build.boot` and `require` the task: 14 | 15 | ```clj 16 | (set-env! :dependencies '[[adzerk/boot-cljs "LATEST" :scope "test"] 17 | [powerlaces/boot-figreload "LATEST" :scope "test"] 18 | [pandeiro/boot-http "0.7.6" :scope "test"] 19 | 20 | [adzerk/boot-cljs-repl "0.3.3" :scope "test"] 21 | [com.cemerick/piggieback "0.2.1" :scope "test"] 22 | [weasel "0.7.0" :scope "test"] 23 | [org.clojure/tools.nrepl "0.2.12" :scope "test"]]) 24 | 25 | (require '[adzerk.boot-cljs :refer [cljs]] 26 | '[adzerk.boot-cljs-repl :refer [cljs-repl]] 27 | '[powerlaces.boot-figreload :refer [reload]] 28 | '[pandeiro.boot-http :refer [serve]]) 29 | ``` 30 | 31 | Add the task to your development pipeline **before `(cljs ...)`**: 32 | 33 | ```clj 34 | (deftask dev [] 35 | (comp (serve) 36 | (watch) 37 | (reload) 38 | (cljs-repl) 39 | (cljs :source-map true 40 | :optimizations :none))) 41 | ``` 42 | 43 | ### Dirac 44 | 45 | Boot-figreload is compatible with Dirac, enabling REPL evaluation in-browser on top of Figwheel's reloading. 46 | 47 | Your `dev` task could therefore become: 48 | 49 | ```clj 50 | (set-env! :dependencies '[[adzerk/boot-cljs "LATEST" :scope "test"] 51 | [powerlaces/boot-figreload "LATEST" :scope "test"] 52 | [pandeiro/boot-http "0.7.6" :scope "test"] 53 | 54 | ;; Dirac and cljs-devtoos 55 | [binaryage/dirac "RELEASE" :scope "test"] 56 | [binaryage/devtools "RELEASE" :scope "test"] 57 | [powerlaces/boot-cljs-devtools "0.2.0" :scope "test"] 58 | 59 | [adzerk/boot-cljs-repl "0.3.3" :scope "test"] 60 | [com.cemerick/piggieback "0.2.1" :scope "test"] 61 | [weasel "0.7.0" :scope "test"] 62 | 63 | ;; Has to be `0.2.13` 64 | [org.clojure/tools.nrepl "0.2.13" :scope "test"]]) 65 | 66 | (require '[adzerk.boot-cljs :refer [cljs]] 67 | '[adzerk.boot-cljs-repl :refer [cljs-repl]] 68 | '[powerlaces.boot-figreload :refer [reload]] 69 | '[powerlaces.boot-cljs-devtools :refer [dirac cljs-devtools]] 70 | '[pandeiro.boot-http :refer [serve]]) 71 | 72 | ... 73 | 74 | (deftask dev [D with-dirac bool "Enable Dirac Devtools."] 75 | (comp (serve) 76 | (watch) 77 | (cljs-devtools) 78 | (reload) 79 | (if-not with-dirac 80 | (cljs-repl) 81 | (dirac)) 82 | (cljs :source-map true 83 | :optimizations :none 84 | :compiler-options {:external-config 85 | {:devtools/config {:features-to-install [:formatters :hints] 86 | :fn-symbol "λ" 87 | :print-config-overrides true}}}))) 88 | 89 | ``` 90 | 91 | ## Node.js 92 | 93 | It should work out of the box. Two things to be aware of: 94 | 95 | * you need to have a `main.cljs.edn` like: 96 | 97 | ```clojure 98 | {:compiler-options {:target :nodejs} 99 | :init-fns [server.core/main]} 100 | ``` 101 | 102 | * you need to launch your built artifact using node: 103 | 104 | ```shell 105 | $ cd target 106 | $ node main.js 107 | ``` 108 | 109 | ## Figwheel Integration Status 110 | 111 | Ok this is a super alpha of the figwheel client in `boot-reload`. 112 | 113 | At the moment the implemented server to client messages are: 114 | 115 | - [x] `:files-changed` 116 | - [x] `:compile-warning` 117 | - [x] `:compile-failed` 118 | - [x] `:css-files-changed` 119 | 120 | Whereas the implemented [client to server](https://github.com/arichiardi/lein-figwheel/blob/boot-reload-changes/sidecar/src/figwheel_sidecar/components/figwheel_server.clj#L75) messages are: 121 | 122 | - [ ] `"file-selected"` 123 | - [ ] `"callback"` 124 | 125 | ### Other tasks to complete: 126 | 127 | - [x] Inject the Figwheel bootstrap script 128 | - [x] Handle individual `js-onload` per build id (untested but there) 129 | - [x] Figwheel version 130 | - [x] Use Figwheel [init code](https://github.com/bhauman/lein-figwheel/blob/cc2d188ab041fc92551d3c4a8201729c47fe5846/sidecar/src/figwheel_sidecar/build_middleware/injection.clj#L171) (?) 131 | - [ ] Handle `boot-reload`'s `:asset-host` in Figwheel ([link to comments](https://github.com/adzerk-oss/boot-reload/commit/e27e330d9f688875ba19d56e825cd9e81013e58e#commitcomment-20350456)) 132 | - [ ] Pass the right `:open-file` option to Figwheel 133 | - [ ] Solve the "first message lost" problem with a message queue (?) 134 | - [x] Assert needed dependencies 135 | - [ ] Repl integration (at the moment supported via [boot-cljs-repl][3]) 136 | 137 | ### To be thorougly tested: 138 | 139 | - [x] Node client 140 | - [ ] Web-worker client 141 | - [ ] Trigger of multiple `js-onload`s 142 | 143 | ## Additional Info 144 | 145 | You can see the options available on the command line: 146 | 147 | ```bash 148 | boot reload --help 149 | ``` 150 | 151 | or in the REPL: 152 | 153 | ```clj 154 | boot.user=> (doc reload) 155 | ``` 156 | 157 | ## Examples 158 | 159 | For an up-to-date demo project check [figreload-demo][4]. 160 | 161 | Legacy examples of how to use `reload` in development can be useful as well. See 162 | [Boot templates and example projects][5] in the ClojureScript wiki. 163 | 164 | ## License 165 | 166 | Copyright © 2014 Adzerk
167 | Copyright © 2015-2016 Juho Teperi
168 | Copyright © 2017 Juho Teperi and Andrea Richiardi 169 | 170 | Distributed under the Eclipse Public License either version 1.0 or (at 171 | your option) any later version. 172 | 173 | [1]: https://github.com/boot-clj/boot 174 | [2]: https://github.com/bhauman/lein-figwheel 175 | [3]: https://github.com/adzerk-oss/boot-cljs-repl 176 | [4]: https://github.com/arichiardi/figreload-demo 177 | [5]: https://github.com/clojure/clojurescript/wiki#boot 178 | -------------------------------------------------------------------------------- /boot.properties: -------------------------------------------------------------------------------- 1 | #http://boot-clj.com 2 | #Thu Dec 24 12:31:47 EET 2015 3 | BOOT_VERSION=2.7.2 4 | BOOT_CLOJURE_VERSION=1.8.0 5 | -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | (def figwheel-dependency '[figwheel-sidecar "0.5.15-SNAPSHOT"]) 2 | 3 | (set-env! 4 | :source-paths #{"src"} 5 | :dependencies (conj '[[org.clojure/clojure "1.8.0" :scope "provided"] 6 | [adzerk/bootlaces "0.1.13" :scope "test"] 7 | [adzerk/boot-test "1.1.0" :scope "test"] 8 | [metosin/boot-alt-test "0.3.0" :scope "test"]] 9 | figwheel-dependency)) 10 | 11 | (require '[adzerk.boot-test :refer [test]] 12 | '[adzerk.bootlaces :refer [bootlaces! build-jar push-snapshot push-release]] 13 | '[metosin.boot-alt-test :refer [alt-test]]) 14 | 15 | (def +version+ "0.5.15-SNAPSHOT") 16 | (bootlaces! +version+) 17 | 18 | (task-options! 19 | pom {:project 'powerlaces/boot-figreload 20 | :version +version+ 21 | :description "Boot task to automatically reload page resources in the browser (featuring Figwheel)." 22 | :url "https://github.com/boot-clj/boot-figreload" 23 | :scm {:url "git@github.com:boot-clj/boot-figreload.git"} 24 | :license {"Eclipse Public License" 25 | "http://www.eclipse.org/legal/epl-v10.html"}}) 26 | 27 | (deftask build [] 28 | (comp 29 | (pom) 30 | (jar) 31 | (install))) 32 | 33 | (deftask dev [] 34 | (comp 35 | (watch) 36 | (repl :server true) 37 | (build) 38 | (target))) 39 | 40 | (def snapshot? #(.endsWith +version+ "-SNAPSHOT")) 41 | 42 | (deftask deploy [] 43 | (comp 44 | (build-jar) 45 | (if (snapshot?) 46 | (push-snapshot) 47 | (push-release)))) 48 | 49 | (ns-unmap *ns* 'test) 50 | 51 | (deftask test 52 | "Run the tests once" 53 | [] 54 | (set-env! :source-paths #(conj % "test") 55 | :dependencies #(conj % figwheel-dependency)) 56 | (alt-test)) 57 | -------------------------------------------------------------------------------- /src/powerlaces/boot_figreload.clj: -------------------------------------------------------------------------------- 1 | (ns powerlaces.boot-figreload 2 | {:boot/export-tasks true} 3 | (:require 4 | [boot.core :as b] 5 | [clojure.java.io :as io] 6 | [clojure.set :as set] 7 | [clojure.string :as string] 8 | [boot.pod :as pod] 9 | [boot.file :as file] 10 | [boot.util :as butil] 11 | [boot.core :refer :all] 12 | [boot.from.digest :as digest] 13 | [powerlaces.boot-figreload.util :as util])) 14 | 15 | (def ^:private deps '[[http-kit "2.1.18"] 16 | [figwheel-sidecar "0.5.15-SNAPSHOT"]]) 17 | 18 | (defn- make-pod [] 19 | (future (-> (get-env) (update-in [:dependencies] into deps) pod/make-pod))) 20 | 21 | (defn- changed [before after only-by-re static-files] 22 | (letfn [(maybe-filter-by-re [files] 23 | (if only-by-re 24 | (by-re only-by-re files) 25 | files))] 26 | (when before 27 | (->> (fileset-diff before after :hash) 28 | output-files 29 | maybe-filter-by-re 30 | (not-by-path static-files) 31 | (sort-by :dependency-order) 32 | (reduce #(conj %1 {:relative-path (-> %2 tmp-path) 33 | :dir-path (-> %2 tmp-dir .getCanonicalPath) 34 | :full-path (-> %2 tmp-file .getCanonicalPath)}) 35 | #{}))))) 36 | 37 | (defn- start-server [pod {:keys [ip port ws-host ws-port secure?] :as opts}] 38 | (let [{:keys [ip port]} (pod/with-call-in pod (powerlaces.boot-figreload.server/start ~opts)) 39 | listen-host (cond (= ip "0.0.0.0") "localhost" :else ip) 40 | client-host (cond ws-host ws-host (= ip "0.0.0.0") "localhost" :else ip) 41 | proto (if secure? "wss" "ws")] 42 | (butil/info "Starting reload server on %s\n" (format "%s://%s:%d" proto listen-host port)) 43 | (format "%s://%s:%d" proto client-host (or ws-port port)))) 44 | 45 | (defn- write-bootstrap-ns! [pod parent-path build-config] 46 | (let [[ns-sym ns-path ns-content] (pod/with-call-in pod 47 | (powerlaces.boot-figreload.server/bootstrap-ns 48 | ~build-config)) 49 | f (io/file parent-path ns-path)] 50 | (io/make-parents f) 51 | (butil/info "Writing %s namespace to %s...\n" ns-sym (.getName f)) 52 | (butil/dbug "%s\n" ns-content) 53 | (spit f ns-content))) 54 | 55 | (defn- send-visual! [pod client-opts visual-map] 56 | (pod/with-call-in pod 57 | (powerlaces.boot-figreload.server/send-visual! 58 | ~client-opts 59 | ~visual-map))) 60 | 61 | (defn- send-changed! [pod client-opts change-map] 62 | (pod/with-call-in pod 63 | (powerlaces.boot-figreload.server/send-changed! 64 | ~client-opts 65 | ~change-map))) 66 | 67 | (defn- add-init! 68 | [pod build-config old-spec out-file] 69 | (io/make-parents out-file) 70 | (let [new-spec (pod/with-call-in pod 71 | (powerlaces.boot-figreload.server/add-cljs-edn-init 72 | ~build-config 73 | ~old-spec))] 74 | (butil/info "Adding :require(s) to %s...\n" (.getName out-file)) 75 | (butil/dbug* "%s\n" (butil/pp-str new-spec)) 76 | (->> new-spec 77 | pr-str 78 | (spit out-file)))) 79 | 80 | (defn- relevant-cljs-edns [fileset ids] 81 | (let [relevant (map #(str % ".cljs.edn") ids) 82 | f (if ids 83 | #(b/by-path relevant %) 84 | #(b/by-ext [".cljs.edn"] %))] 85 | (-> fileset b/input-files f))) 86 | 87 | (defn cljs-opts-seq 88 | "Return a sequence of compiler options given boot's .cljs.edn files on 89 | the fileset. If no :adzerk.boot-cljs/opts key is found on the tmpfile, 90 | no entry is added to the output sequence." 91 | [tmpfiles] 92 | (->> tmpfiles 93 | (map #(when-let [opts (:adzerk.boot-cljs/opts %)] 94 | (assoc opts :build-id (-> % b/tmp-file .getPath util/build-id)))) 95 | (remove nil?))) 96 | 97 | (defn make-build-config 98 | "Return an ClojureScript build configuration map akin to leinengen and 99 | figwheel: 100 | https://github.com/bhauman/lein-figwheel#client-side-configuration-options 101 | 102 | The id is last because it is the one arguably more likely to vary." 103 | [client-opts build-id] 104 | ;; See https://github.com/bhauman/lein-figwheel/blob/0283f6d79af630854c1d3071eeb6f0ff8fd41676/sidecar/src/figwheel_sidecar/schemas/config.clj#L602 105 | (-> {:id build-id :figwheel client-opts} 106 | util/remove-nils)) 107 | 108 | (defn read-cljs-edn-spec! 109 | [cljs-edn-tmpfile] 110 | (let [file (tmp-file cljs-edn-tmpfile) 111 | path (tmp-path cljs-edn-tmpfile)] 112 | (if (.exists file) 113 | (try (-> file slurp read-string) 114 | (catch Exception e 115 | (let [] 116 | (throw (ex-info 117 | (format "Cannot read %s" path) 118 | {:cljs-edn {:path path 119 | :content (slurp file)}} 120 | e))))) 121 | (throw (ex-info (format "The file %s does not exist. This might be a bug." (.getAbsolutePath file)) 122 | {:cljs-edn path}))))) 123 | 124 | (deftask reload 125 | "Live reload of page resources in browser via websocket. 126 | 127 | The default configuration starts a websocket server on a random available 128 | port on localhost. 129 | 130 | Open-file option takes three arguments: line number, column number, relative 131 | file path. You can use positional arguments if you need different order. 132 | Arguments shouldn't have spaces. 133 | Examples: 134 | vim --remote +norm%sG%s| %s 135 | emacsclient -n +%s:%s %s 136 | 137 | Client options can also be set in .cljs.edn file, using property :boot-reload, e.g. 138 | :boot-reload {:on-jsload frontend.core/reload}" 139 | 140 | [b ids BUILD_IDS #{str} "Only inject reloading into these builds (= .cljs.edn files)" 141 | ;; Websocket Server 142 | i ip ADDR str "The IP address for the websocket server to listen on. (optional)" 143 | p port PORT int "The port the websocket server listens on. (optional)" 144 | _ ws-port PORT int "The port the websocket will connect to. (optional)" 145 | w ws-host WSADDR str "The websocket host clients connect to. Defaults to current host. (optional)" 146 | s secure bool "Flag to indicate whether the client should connect via wss. Defaults to false." 147 | ;; Client Configuration 148 | ^{:deprecated "--j/--on-jsload should go in the :boot-reload key of your .cljs.edn(s)."} 149 | j on-jsload SYM sym "The callback to call when JS files are reloaded. (deprecated)" 150 | 151 | _ client-opts OPTS edn "Options passed to the client directly (overrides the others)." 152 | v disable-hud bool "Toggle to disable HUD. Defaults to false (visible)." 153 | o open-file CMD str "The command to run when warning or exception is clicked on HUD. Passed to format. (optional)" 154 | ;; Other Configuration - AR - (?) 155 | _ asset-host HOST str "The asset-host where to load files from. Defaults to host of opened page. (optional)" 156 | 157 | ^{:deprecated "--a/--asset-path should go in the boot-cljs :compiler-options key of your .cljs.edn(s)."} 158 | a asset-path PATH str "Sets the output directory for temporary files used during compilation. (deprecated)" 159 | 160 | c cljs-asset-path PATH str "The actual asset path. This is added to the start of reloaded urls. (optional)" 161 | t target-path VAL str "Target path to load files from, used WHEN serving files using file: protocol. (optional)" 162 | _ only-by-re REGEX [regex] "Vector of path regexes (for `boot.core/by-re`) to restrict reloads to only files within these paths (optional)."] 163 | 164 | (let [pod (make-pod) 165 | tmp (tmp-dir!) 166 | prev-pre (atom nil) 167 | prev (atom nil) 168 | first-run? #(empty? @prev-pre) 169 | url (start-server @pod {:ip ip :port port :ws-host ws-host 170 | :ws-port ws-port :secure? secure 171 | :open-file open-file})] 172 | (b/cleanup (pod/with-call-in @pod (powerlaces.boot-figreload.server/stop))) 173 | (fn [next-task] 174 | (fn [fileset] 175 | ;; AR - TODO - pass the :open-file option down to figwheel and figure 176 | ;; out how to handle it 177 | (pod/with-call-in @pod 178 | (powerlaces.boot-figreload.server/set-options {:open-file ~open-file})) 179 | 180 | (let [changed-cljs-edns (relevant-cljs-edns (b/fileset-diff @prev-pre fileset :hash) ids) 181 | client-opts (merge {:project-id (-> (b/get-env) :directories string/join digest/md5) 182 | :websocket-host ws-host 183 | :websocket-url url 184 | :asset-host asset-host} 185 | client-opts)] 186 | (when (> @butil/*verbosity* 2) 187 | (butil/dbug "Client-opts:\n%s\n" (butil/pp-str client-opts))) 188 | (if-not (empty? changed-cljs-edns) 189 | (doseq [f changed-cljs-edns] 190 | (let [spec (read-cljs-edn-spec! f) 191 | path (tmp-path f) ;; this is relative to boot's fileset cache 192 | file (tmp-file f) 193 | build-config (make-build-config (merge client-opts (:boot-reload spec)) 194 | (-> file .getPath util/build-id))] 195 | (when (> @butil/*verbosity* 2) 196 | (butil/dbug "Content of %s:\n%s\n" path (butil/pp-str spec))) 197 | (butil/dbug "Build config:\n%s\n" (butil/pp-str build-config)) 198 | ;; Writing the connection script to the tmp folder 199 | (write-bootstrap-ns! @pod tmp build-config) 200 | ;; Add the init files to the fileset 201 | (add-init! @pod build-config spec (io/file tmp path)))) 202 | 203 | (do (when (first-run?) 204 | (butil/warn "WARNING: No .cljs.edn file found.\n") 205 | (butil/warn "A .cljs.edn file is necessary for advanced features, boot-figreload might misbehave if missing.\n") 206 | (butil/warn "This is especially true for {:boot-reload {:on-jsload ...}}.\n")) 207 | ;; Special case: boot-cljs used without .cljs.edn 208 | ;; in that case we can just create a bootstrap namespace for 209 | ;; the "main" build id. 210 | ;; We do it only in case the diffing with the previous run did 211 | ;; not contain added/changed .cljs.edn itself. 212 | (when (empty? (relevant-cljs-edns fileset ids)) 213 | (butil/dbug "No other .cljs.edn found on the fileset.") 214 | (let [build-config (make-build-config client-opts "main")] 215 | (butil/dbug* "Build config:\n%s\n" (butil/pp-str build-config)) 216 | (write-bootstrap-ns! @pod tmp build-config)))))) 217 | 218 | (reset! prev-pre fileset) 219 | (let [fileset (-> fileset (b/add-resource tmp) commit!) 220 | fileset (try 221 | (next-task fileset) 222 | (catch Exception e 223 | ;; FIXME: Supports only single error, e.g. less compiler 224 | ;; can give multiple errors. 225 | (if (and (not disable-hud) 226 | (or (= :boot-cljs (:from (ex-data e))) 227 | (:powerlaces.boot-figreload/exception (ex-data e)))) 228 | (send-visual! @pod client-opts {:exception (util/serialize-exception e)})) 229 | (throw e)))] 230 | (let [cljs-edns (relevant-cljs-edns fileset ids) 231 | ;; cljs uses specific key for now 232 | ;; but any other file can contain warnings for boot-reload 233 | warnings (concat (mapcat :adzerk.boot-cljs/warnings cljs-edns) 234 | (mapcat :adzerk.boot-reload/warnings (b/input-files fileset))) 235 | cljs-opts-seq (cljs-opts-seq cljs-edns) 236 | static-files (->> cljs-edns 237 | (map b/tmp-path) 238 | (map(fn [x] (string/replace x #"\.cljs\.edn$" ".js"))) 239 | set)] 240 | (butil/dbug* "Compile opts:\n%s\n" (butil/pp-str cljs-opts-seq)) 241 | (when (> @butil/*verbosity* 2) 242 | (butil/dbug "Warnings:\n%s\n" (butil/pp-str warnings)) 243 | (doseq [edn cljs-edns] 244 | (butil/dbug "Meta on %s:\n%s\n" (:path edn) (butil/pp-str (select-keys edn []))))) 245 | (if-not disable-hud 246 | (send-visual! @pod client-opts {:warnings warnings})) 247 | ;; Only send changed files when there are no warnings. As prev is 248 | ;; updated only when changes are sent, changes are queued until 249 | ;; they can be sent 250 | ;; 251 | ;; AR - the above assumption changes when using figwheel's client, 252 | ;; where the event order (unfortunately) matters. 253 | (send-changed! @pod 254 | client-opts 255 | {:target-path target-path 256 | :project-dirs (map str (util/project-dirs)) 257 | :cljs-asset-path cljs-asset-path 258 | :cljs-opts-seq cljs-opts-seq 259 | :change-set (changed @prev fileset only-by-re static-files)}) 260 | (reset! prev fileset) 261 | fileset)))))) 262 | -------------------------------------------------------------------------------- /src/powerlaces/boot_figreload/figwheel.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:doc "Figwheel bindings and/or functions copied over." 2 | :author "Andrea Richiardi"} 3 | powerlaces.boot-figreload.figwheel 4 | (:require [boot.pod :as pod] 5 | [boot.util :as butil] 6 | [clojure.string :as str] 7 | [clojure.walk :as walk] 8 | [clojure.java.io :as io] 9 | [figwheel-sidecar.config :as config] 10 | [figwheel-sidecar.build-middleware.javascript-reloading :as js-reload] 11 | [figwheel-sidecar.cljs-utils.exception-parsing :as ex-parsing] 12 | [figwheel-sidecar.build-middleware.injection :as injection] 13 | [cljs.compiler] 14 | [cljs.closure] 15 | [powerlaces.boot-figreload.util :as util] 16 | [powerlaces.boot-figreload.messages :as msgs])) 17 | 18 | ;;;;;;;;;;;;;;;;;;;; 19 | ;; DIRECT IMPORTS ;; 20 | ;;;;;;;;;;;;;;;;;;;; 21 | 22 | (def ^{:doc "Generate the bootstrap namespace path string" 23 | :arglists '([build-config])} 24 | bootstrap-ns-name 25 | (var-get #'figwheel-sidecar.build-middleware.injection/figwheel-connect-ns-name)) 26 | 27 | (def ^{:doc "Generate the bootstrap namespace path string" 28 | :arglists '([build-config])} 29 | bootstrap-ns-path 30 | (var-get #'figwheel-sidecar.build-middleware.injection/figwheel-connect-ns-path)) 31 | 32 | (def ^{:doc "Generate the bootstrap namespace content" 33 | :arglists '([build-config])} 34 | bootstrap-ns-content 35 | (var-get #'figwheel-sidecar.build-middleware.injection/generate-connect-script)) 36 | 37 | (defn wrap-msg 38 | "Add common fields to the message 39 | 40 | Note that msg keys always override in case of conflict." 41 | ([msg] (wrap-msg msg nil)) 42 | ([msg opts] 43 | (-> opts 44 | (select-keys [:project-id :build-id]) 45 | (assoc :figwheel-version config/_figwheel-version_) 46 | (merge msg) 47 | util/remove-nils))) 48 | 49 | ;; Reusing figwheel's make-sendable-file is not possible at the moment 50 | ;; https://github.com/bhauman/lein-figwheel/blob/e47da1658a716f83888e5a5164ee88e59b2d8c1e/sidecar/src/figwheel_sidecar/build_middleware/notifications.clj#L78 51 | ;; (intern *ns* 'make-sendable-file (var-get #'notifications/make-sendable-file)) 52 | 53 | (defn- client-path 54 | "Return the path that the client uses (relative to :asset-path) 55 | 56 | The input file map " 57 | [compile-opts file-map] 58 | (assert (:relative-path file-map) "The file-map is missing some fields, this is a bug.") 59 | (-> (str/replace (:relative-path file-map) 60 | (or (:asset-path file-map) "") 61 | "") 62 | (str/replace #"^/" ""))) 63 | 64 | (defn- guess-namespace 65 | [compile-opts file-map] 66 | (assert (:relative-path file-map) "The file-map is missing some fields, this is a bug.") 67 | (assert (:full-path file-map) "The file-map is missing some fields, this is a bug.") 68 | ;; Workaround for: https://github.com/bhauman/lein-figwheel/pull/537 69 | ;; 70 | ;; No figwheel-sidecar.build-middleware.javascript-reloading/guess-namespace 71 | ;; because deemed broken (sorry Bruce, it will be fixed I am sure). 72 | ;; We try to create the absolute path relative to the project root first. 73 | (let [js-file-attempts (conj (->> (util/project-dirs) 74 | (mapv #(io/file % (:relative-path file-map))) 75 | (filter #(.exists %)) 76 | (mapv str)) 77 | (:full-path file-map))] 78 | (butil/dbug* "Guessing namespace for %s\n" (butil/pp-str js-file-attempts)) 79 | (assert (every? #(re-find #"\.js$" %) js-file-attempts) 80 | "Cannot guess namespace for files that are not Javascript. This is an bug, please report it.") 81 | (->> js-file-attempts 82 | (mapv cljs.closure/parse-js-ns) 83 | first 84 | :provides 85 | first 86 | cljs.compiler/munge))) 87 | 88 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 89 | ;; :msg-name :files-changed ;; 90 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 91 | 92 | (defn- sendable-js-map 93 | "Make a (Javascript) file map sendable according to figwheel protocol. 94 | 95 | Mimicking the function to 96 | figwheel-sidecar.build-middleware.notifications/make-sendable-file 97 | 98 | Sample return map: 99 | {:file \"resources/public/js/compiled/out/test_fig/core.js\" 100 | :namespace \"test_fig.core\" 101 | :type :namespace}" 102 | [js-and-opts] 103 | {:namespace (guess-namespace (:cljs-opts js-and-opts) js-and-opts) 104 | :file (client-path (:cljs-opts js-and-opts) js-and-opts) 105 | :type :namespace}) 106 | 107 | (defn- sendable-css-map 108 | "Make a (Javascript) file map sendable according to figwheel protocol. 109 | 110 | Mimicking the function to 111 | figwheel-sidecar.build-middleware.notifications/make-sendable-file 112 | 113 | Sample return map: 114 | {:file \"resources/public/js/compiled/out/test_fig/core.js\" 115 | :namespace \"test_fig.core\" 116 | :type :namespace}" 117 | [js-and-opts] 118 | {:file (client-path (:cljs-opts js-and-opts) js-and-opts) 119 | :type :css}) 120 | 121 | (defmethod msgs/file-payload-by-extension :js 122 | [opts [ext change-maps]] 123 | (when (seq change-maps) 124 | (-> {:msg-name :files-changed 125 | :files (mapv sendable-js-map change-maps) 126 | :figwheel-meta {"figwheel.client.utils" {:figwheel-no-load true}}} 127 | (wrap-msg opts)))) 128 | 129 | (defmethod msgs/file-payload-by-extension :css 130 | [opts [ext change-maps]] 131 | (when (seq change-maps) 132 | (-> {:msg-name :css-files-changed 133 | :files (mapv sendable-css-map change-maps)} 134 | (wrap-msg opts)))) 135 | 136 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 137 | ;; :msg-name :compile-warning ;; 138 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 139 | 140 | ;; AR - TODO handle multiple warnings, now we return only one 141 | (defmethod msgs/visual-payload-by-type :warnings 142 | [opts [type warning-maps]] 143 | (when (seq warning-maps) 144 | (-> {:msg-name :compile-warning 145 | :message (ex-parsing/parse-warning (first warning-maps))} 146 | (wrap-msg opts)))) 147 | 148 | (defn trim-source-paths 149 | "Remove any :source-paths or :resource-paths parent from path" 150 | [path] 151 | (->> (:source-paths pod/env) 152 | (filter #(str/index-of path %)) 153 | (map #(-> path 154 | (str/replace % "") 155 | (str/replace-first #"^\/" ""))))) 156 | 157 | (defn- clean-class-form 158 | [form] 159 | (update form 1 #(-> % symbol resolve))) 160 | 161 | (defn- relativize-data-form 162 | "Replace absolute paths with relative ones" 163 | [relative-path form] 164 | (let [file (get-in form [1 :file])] 165 | (cond 166 | (or (not form) 167 | (not (string? relative-path)) 168 | (not (string? file))) form 169 | 170 | ;; We replace only if it is a substring of the original 171 | (some (partial str/includes? file) (trim-source-paths relative-path)) 172 | (assoc-in form [1 :file] relative-path) 173 | 174 | :else form))) 175 | 176 | (defn- relativize-cause-form 177 | "Replace absolute paths with relative ones where necessary" 178 | [relative-path form] 179 | (let [file (get-in form [1 :data :file]) 180 | message (get-in form [1 :message])] 181 | (cond 182 | (or (not form) 183 | (not (string? file)) 184 | (not (string? message))) form 185 | 186 | ;; We replace only if it is there is a substring in the original msg 187 | (str/includes? message file) 188 | (assoc-in form [1 :message] (-> message 189 | (str/replace file relative-path) 190 | (str/replace "file:" ""))) 191 | 192 | :else form))) 193 | 194 | (defn figwheelify-exception 195 | "Boot-cljs to figwheel exception map 196 | 197 | Just to make it super clear, we receive a serialized exception map 198 | from boot-cljs and we want to convert it to a format that figwheel can 199 | parse." 200 | [ex] 201 | (let [relative-file (when (= :boot-cljs (get-in ex [:data :from])) 202 | (get-in ex [:data :file]))] 203 | (walk/prewalk #(cond 204 | ;; Convert string in :class to a java.lang.Class obj 205 | (util/map-entry-with-key? % :class) (clean-class-form %) 206 | ;; Make all the :data :file entries relative 207 | ;; I assume (!) the first exception contains the relative 208 | ;; path 209 | (and relative-file (util/map-entry-with-key? % :cause)) (relativize-cause-form relative-file %) 210 | (and relative-file (util/map-entry-with-key? % :data)) (relativize-data-form relative-file %) 211 | :else %) 212 | ex))) 213 | 214 | (defmethod msgs/visual-payload-by-type :exception 215 | [opts [type ex-map]] 216 | ;; AR - this is real hammering 217 | (-> {:msg-name :compile-failed 218 | :exception-data (-> ex-map 219 | figwheelify-exception 220 | (ex-parsing/parse-inspected-exception opts))} 221 | (wrap-msg opts))) 222 | 223 | (comment 224 | (def ex-map {:class "clojure.lang.ExceptionInfo", :message "Parameter declaration \".info\" should be a vector at line 10, column 1 in file src/figreload_demo/core.cljs\n", :data {:file "src/figreload_demo/core.cljs", :line 10, :column 1, :tag :cljs/analysis-error, :from :boot-cljs, :boot.util/omit-stacktrace? true}, :cause {:class "clojure.lang.ExceptionInfo", :message "failed compiling file:/home/arichiardi/.boot/cache/tmp/home/arichiardi/git/figreload-demo/mbk/7of19k/figreload_demo/core.cljs", :data {:file "/home/arichiardi/.boot/cache/tmp/home/arichiardi/git/figreload-demo/mbk/7of19k/figreload_demo/core.cljs"}, :cause {:class "clojure.lang.ExceptionInfo", :message "Parameter declaration \".info\" should be a vector at line 10 /home/arichiardi/.boot/cache/tmp/home/arichiardi/git/figreload-demo/mbk/7of19k/figreload_demo/core.cljs", :data {:file "/home/arichiardi/.boot/cache/tmp/home/arichiardi/git/figreload-demo/mbk/7of19k/figreload_demo/core.cljs", :line 10, :column 1, :tag :cljs/analysis-error}, :cause {:class "java.lang.IllegalArgumentException", :message "Parameter declaration \".info\" should be a vector", :data nil, :cause nil}}}}) 225 | ) 226 | -------------------------------------------------------------------------------- /src/powerlaces/boot_figreload/messages.clj: -------------------------------------------------------------------------------- 1 | (ns ^{:doc "Message management." 2 | :author "Andrea Richiardi"} 3 | powerlaces.boot-figreload.messages) 4 | 5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 6 | ;; FILE CHANGED NOTIFICATIONS ;; 7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 8 | 9 | (defn- file-map-matches? 10 | [file-map compile-opts] 11 | (assert (:asset-path compile-opts) "The compiler options :asset-path cannot be nil. This might be a bug.") 12 | (assert (:relative-path file-map) "The relative path in the file-map cannot be nil. This might be a bug.") 13 | (re-find (re-pattern (:asset-path compile-opts)) (:relative-path file-map))) 14 | 15 | (defn- assign-cljs-opts 16 | "Assign the correct cljs options to the file-map based on pred. 17 | 18 | The (fn [data compile-opts] ...) predicate decides what \"correct\" 19 | means, returning true or false according to the input data and the 20 | candidate compile-opts. 21 | 22 | The correct options will be assigned under the :cljs-opts key." 23 | [pred cljs-opts-seq file-maps] 24 | (mapv #(if-let [opts (->> cljs-opts-seq 25 | (filter (partial pred %)) 26 | first)] 27 | (assoc % :cljs-opts opts) 28 | %) 29 | file-maps)) 30 | 31 | (def ^{:private true 32 | :doc "Assign the correct cljs options to the correct file-map based on its path" 33 | :arglists '([cljs-opts-seq file-maps])} 34 | assign-cljs-opts-by-path 35 | (partial assign-cljs-opts file-map-matches?)) 36 | 37 | (defmulti file-payload-by-extension 38 | "Calculate payloads of change-maps based on extension. 39 | 40 | The first parameter is obviously the client option map, while the 41 | second is a vector containing extension first and then a seq of file 42 | maps: 43 | 44 | [:css [[file-map1] [file-map2]]]" 45 | (fn [client-opts [ext change-maps]] ext)) 46 | 47 | (defmethod file-payload-by-extension :default 48 | [_ _] 49 | (assert false "This should never happen, did you forget to handle a file extension?")) 50 | 51 | (defn changed-messages 52 | "Return (file) change-related messages" 53 | [client-opts watch-map] 54 | (let [change-maps-by-ext (select-keys 55 | (->> watch-map 56 | :change-set 57 | (assign-cljs-opts-by-path (:cljs-opts-seq watch-map)) 58 | (group-by #(->> (:relative-path %) 59 | (re-find #"\.(\p{Alnum}+$)") 60 | second 61 | keyword))) 62 | [:css :js])] 63 | (map (partial file-payload-by-extension client-opts) change-maps-by-ext))) 64 | 65 | ;;;;;;;;;;;;;;;;;;;;;;;;;; 66 | ;; VISUAL NOTIFICATIONS ;; 67 | ;;;;;;;;;;;;;;;;;;;;;;;;;; 68 | 69 | (defmulti visual-payload-by-type 70 | "Calculate payloads of a visual-map based on it type 71 | 72 | The first parameter is obviously the client option map, while the 73 | second is a vector [type data]. The dispatch is on the type." 74 | (fn [client-opts [type data]] type)) 75 | 76 | (defmethod visual-payload-by-type :default 77 | [_ _] 78 | (assert false "This should never happen, did you forget to handle a visual notification type?")) 79 | 80 | (defn visual-message 81 | "Return a visual message. 82 | 83 | It expects something like: 84 | {:warnings [{:k1 :v1} {:k2 :v2} ...]} 85 | ^- type ^- data (not only seqs, it depends on the the type)" 86 | [client-opts visual-map] 87 | ;; AR - I know I know we need clojure.spec 88 | (visual-payload-by-type client-opts (first (seq visual-map)))) 89 | -------------------------------------------------------------------------------- /src/powerlaces/boot_figreload/server.clj: -------------------------------------------------------------------------------- 1 | (ns powerlaces.boot-figreload.server 2 | (:require 3 | [clojure.java.io :as io] 4 | [boot.util :as util] 5 | [org.httpkit.server :as http] 6 | [clojure.string :as string] 7 | [powerlaces.boot-figreload.figwheel :as figwheel] 8 | [powerlaces.boot-figreload.messages :as msgs]) 9 | (:import 10 | [java.io IOException])) 11 | 12 | (def options (atom {:open-file nil})) 13 | (def clients (atom #{})) 14 | (def stop-fn (atom nil)) 15 | 16 | (defn set-options [opts] 17 | (reset! options opts)) 18 | 19 | (defn bootstrap-ns 20 | "Computes the bootstrap namespace 21 | 22 | Return [ns-sym ns-path ns-content]." 23 | [build-config] 24 | [(figwheel/bootstrap-ns-name build-config) 25 | (figwheel/bootstrap-ns-path build-config) 26 | (->> build-config 27 | figwheel/bootstrap-ns-content 28 | (map util/pp-str) 29 | (interpose "\n") 30 | (apply str))]) 31 | 32 | ;; AR The cousin of: 33 | ;; https://github.com/bhauman/lein-figwheel/blob/cc2d188ab041fc92551d3c4a8201729c47fe5846/sidecar/src/figwheel_sidecar/build_middleware/injection.clj#L171 34 | (defn- add-cljs-edn-init 35 | "Add requires and other init stuff to the .cljs.edn spec 36 | 37 | Return the new cljs.edn spec as data (not a string)." 38 | [build-config cljs-spec] 39 | (update cljs-spec :require #(->> (conj % 40 | (-> build-config 41 | figwheel/bootstrap-ns-name 42 | symbol) 43 | (when (get-in build-config [:figwheel :devcards]) 44 | 'devcards.core)) 45 | (into #{}) 46 | (remove nil?) 47 | vec))) 48 | 49 | ;;;;;;;;;;;;;;; 50 | ;; WEBSOCKET ;; 51 | ;;;;;;;;;;;;;;; 52 | 53 | (defn send-changed! 54 | ([change-map] (send-changed! {} change-map)) 55 | ([opts change-map] 56 | (when (> @boot.util/*verbosity* 2) 57 | (util/dbug "Watch received:\n%s\n" (util/pp-str change-map))) 58 | (doseq [channel @clients] 59 | (let [payloads (->> change-map 60 | (msgs/changed-messages opts) 61 | (mapv #(merge % (select-keys opts [:figwheel-version]))))] 62 | (run! #(let [payload (util/pp-str %)] 63 | (util/dbug "Sending:\n%s\n" payload) 64 | (http/send! channel payload)) 65 | payloads))))) 66 | 67 | (defn send-visual! 68 | "Send a visual notification to the connected clients. 69 | 70 | A visual map is a map that has its key equal to the type (:warning, 71 | exception, ...) of the notification and its value equal to a sequence 72 | of payloads." 73 | ([visual-map] (send-visual! {} visual-map)) 74 | ([opts visual-map] 75 | (when (> @boot.util/*verbosity* 2) 76 | (util/dbug "Watch received:\n%s\n" visual-map)) 77 | (doseq [channel @clients] 78 | ;; AR - TODO handle multiple warnings and exceptions 79 | (let [payload (->> visual-map 80 | (msgs/visual-message opts) 81 | (merge (select-keys opts [:figwheel-version])))] 82 | (when-not (empty? payload) 83 | (util/dbug "Sending:\n%s\n" payload) 84 | (http/send! channel (util/pp-str payload))))))) 85 | 86 | ;;;;;;;;;;;;;;; 87 | ;; WEBSOCKET ;; 88 | ;;;;;;;;;;;;;;; 89 | 90 | (defmulti handle-message (fn [channel message] (:figwheel-event message))) 91 | 92 | (defmethod handle-message :default [channel message] 93 | (util/warn "Received Figwheel message %s: not supported at this time\n" (:figwheel-event message)) 94 | (util/dbug* "Figwheel Message:\n%s\n" (util/pp-str message))) 95 | 96 | (defn connect! [channel] 97 | (util/dbug "Channel \"%s\" opened...\n" (str channel)) 98 | 99 | (swap! clients conj channel) 100 | (when (> @boot.util/*verbosity* 2) 101 | (util/dbug "Connected clients %s\n" (mapv str @clients))) 102 | 103 | (http/on-close channel (fn [_] (swap! clients disj channel))) 104 | (http/on-receive channel (fn [data] 105 | (when (> @boot.util/*verbosity* 2) 106 | (util/dbug "Websocket received:\n%s\n" (util/pp-str data))) 107 | (handle-message channel (read-string data)))) 108 | 109 | (util/info "New websocket client connected!\n")) 110 | 111 | (defn handler [request] 112 | (if-not (:websocket? request) 113 | {:status 501 :body "Websocket connections only."} 114 | (do (when (> @boot.util/*verbosity* 2) 115 | (util/dbug* "Websocket received:\n%s\n" (util/pp-str request))) 116 | (http/with-channel request channel (connect! channel))))) 117 | 118 | (defn start 119 | [{:keys [ip port] :as opts}] 120 | (let [o {:ip (or ip "0.0.0.0") :port (or port 0)} 121 | stop-fn* (http/run-server handler o)] 122 | (reset! stop-fn stop-fn*) 123 | (assoc o :port (-> stop-fn* meta :local-port)))) 124 | 125 | (defn stop [] 126 | (when @stop-fn 127 | (@stop-fn) 128 | (reset! stop-fn nil))) 129 | -------------------------------------------------------------------------------- /src/powerlaces/boot_figreload/util.clj: -------------------------------------------------------------------------------- 1 | (ns powerlaces.boot-figreload.util 2 | (:require [clojure.string :as str] 3 | [clojure.java.io :as io] 4 | [clojure.walk :as walk] 5 | [boot.file :as file] 6 | [boot.from.digest :as digest]) 7 | (:import [java.io File])) 8 | 9 | ;; From cljs/analyzer.cljc 10 | (def js-reserved 11 | #{"arguments" "abstract" "boolean" "break" "byte" "case" 12 | "catch" "char" "class" "const" "continue" 13 | "debugger" "default" "delete" "do" "double" 14 | "else" "enum" "export" "extends" "final" 15 | "finally" "float" "for" "function" "goto" "if" 16 | "implements" "import" "in" "instanceof" "int" 17 | "interface" "let" "long" "native" "new" 18 | "package" "private" "protected" "public" 19 | "return" "short" "static" "super" "switch" 20 | "synchronized" "this" "throw" "throws" 21 | "transient" "try" "typeof" "var" "void" 22 | "volatile" "while" "with" "yield" "methods" 23 | "null" "constructor"}) 24 | 25 | (defn build-id 26 | "Return the build id from the .cljs.edn file path (as string)." 27 | [cljs-edn-path] 28 | (assert (and (re-find #"\.cljs\.edn$" cljs-edn-path) 29 | (re-find #"(\/|\\)" cljs-edn-path)) 30 | (format "Build-id expects a .cljs.edn file path. Received %s, this might be a bug." cljs-edn-path)) 31 | (let [bid (str/replace cljs-edn-path #"(.*)(\/|\\)(\w+)(\.cljs\.edn)$" "$3")] 32 | (cond-> bid 33 | (contains? js-reserved bid) (str "$") 34 | true (str "_" (->> cljs-edn-path digest/sha-1 (take 8) (str/join)))))) 35 | 36 | ;; 37 | ;; Exception serialization 38 | ;; Also see: https://github.com/boot-clj/boot/issues/553 39 | ;; 40 | 41 | (defn safe-data [data] 42 | (walk/postwalk 43 | (fn [x] 44 | (cond 45 | (instance? File x) (.getPath x) 46 | :else x)) 47 | data)) 48 | 49 | (defn serialize-exception 50 | "Serializes given exception keeping original message, stack-trace, cause stack 51 | and ex-data for ExceptionInfo. 52 | 53 | Certain types in ex-data are converted to strings. Currently this includes 54 | Files." 55 | [e] 56 | {:class (-> e type .getName) ;; AR - this is ignored by the deserializer 57 | :message (.getMessage e) 58 | :data (safe-data (ex-data e)) ;; AR - no :ex-data, figwheel likes :data 59 | :cause (when-let [cause (.getCause e)] 60 | (serialize-exception cause))}) 61 | 62 | (defn remove-nils [m] 63 | (let [f (fn [x] 64 | (if (map? x) 65 | (let [kvs (filter (comp not nil? second) x)] 66 | (if (empty? kvs) nil (into {} kvs))) 67 | x))] 68 | (walk/postwalk f m))) 69 | 70 | (defn map-entry-with-key? 71 | [form k] 72 | (and (vector? form) (= k (first form)))) 73 | 74 | (defn project-root 75 | "Return the project root as string." 76 | [] ^String 77 | ;; AR - consider taking a more rigorous approach, for instance: 78 | ;; https://github.com/clojure-emacs/refactor-nrepl/blob/v2.3.1/src/refactor_nrepl/core.clj#L61 79 | (System/getProperty "user.dir")) 80 | 81 | (defn project-dirs 82 | "Return all project dirs added to either source, resources or assets. 83 | 84 | The output is a vector of java.io.File objects and uses 85 | `fake.class.path` + `user.dir` for doing its job." 86 | [] 87 | (let [files-on-cp (map io/file (str/split (System/getProperty "fake.class.path") #":")) 88 | project-root-path (.toPath (io/file (project-root)))] 89 | (->> files-on-cp 90 | (filter #(.isDirectory %)) 91 | (filter #(.startsWith (.toPath %) project-root-path))))) 92 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "1.8.0" 6 | BOOT_CLOJURE_VERSION=1.8.0 boot test 7 | 8 | echo 9 | echo "1.7.0" 10 | BOOT_CLOJURE_VERSION=1.7.0 boot test 11 | 12 | echo 13 | echo "1.6.0" 14 | BOOT_CLOJURE_VERSION=1.6.0 boot test 15 | -------------------------------------------------------------------------------- /test/powerlaces/boot_figreload/util_test.clj: -------------------------------------------------------------------------------- 1 | (ns powerlaces.boot-figreload.util-test 2 | (:require [powerlaces.boot-figreload.util :as util] 3 | [clojure.java.io :as io] 4 | [clojure.test :as test :refer [deftest is]])) 5 | 6 | (deftest build-id 7 | (is (= (re-find #"^main-.*" (util/build-id "/my/project/src/my_namespace/main.cljs.edn"))) "Should correctly take the part before .cljs.edn as build-id") 8 | (is (= (re-find #"^transient$-.*" (util/build-id "/my/project/src/my_namespace/transient.cljs.edn"))) "Should correctly escape JS reserved words as build-id") 9 | (is (= (re-find #"^main-.*" (util/build-id "/my/project/src/scripts/main.cljs.edn"))) "Should correctly get only the .cljs.edn filename as build-id") 10 | (is (thrown? java.lang.AssertionError (util/build-id "transient.edn")) "Should throw if we are not passing a .cljs.edn file") 11 | (is (thrown? java.lang.AssertionError (util/build-id "transient.cljs.edn")) "Should throw if we are not passing a path")) 12 | --------------------------------------------------------------------------------