├── .clj-kondo └── config.edn ├── .extras └── logo.png ├── .github ├── FUNDING.yml └── workflows │ └── build-deploy.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── deps.edn ├── dev └── user.clj ├── fixtures ├── core_test │ ├── a.clj │ ├── b.clj │ ├── c.clj │ ├── custom_file_type.repl │ ├── d.clj │ ├── double.clj │ ├── double_b.clj │ ├── e.clj │ ├── err_parse.clj │ ├── err_runtime.clj │ ├── f.clj │ ├── g.clj │ ├── h.clj │ ├── i.clj │ ├── j.clj │ ├── k.clj │ ├── l.clj │ ├── m.clj │ ├── n.clj │ ├── no_ns.clj │ ├── no_reload.clj │ ├── no_unload.clj │ ├── o.clj │ ├── split.clj │ ├── split_part.clj │ └── two_nses.clj └── keep_test │ └── clj_reload │ ├── dependency.clj │ ├── keep_custom.clj │ ├── keep_defprotocol.clj │ ├── keep_defrecord.clj │ ├── keep_deftype.clj │ ├── keep_downstream.clj │ ├── keep_unsupported.clj │ ├── keep_upstream.clj │ └── keep_vars.clj ├── project.clj ├── script ├── repl.sh └── test.sh ├── src └── clj_reload │ ├── core.clj │ ├── keep.clj │ ├── parse.clj │ └── util.clj └── test └── clj_reload ├── core_test.clj ├── keep_test.clj ├── parse_test.clj └── test_util.clj /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as 2 | {clj-reload.util/for-map clojure.core/for 3 | clj-reload.util/for-set clojure.core/for}} -------------------------------------------------------------------------------- /.extras/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonsky/clj-reload/e791990ae4ff78a6112b078ab1144a40197d570b/.extras/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [tonsky] 2 | patreon: tonsky -------------------------------------------------------------------------------- /.github/workflows/build-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags: 8 | - '[0-9]+.[0-9]+.[0-9]+' 9 | paths: 10 | - '.github/workflows/**' 11 | - 'src/**' 12 | - 'test/**' 13 | - project.clj 14 | - deps.edn 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | env: 21 | CLOJARS_TOKEN: ${{ secrets.CLOJARS_DEPLOY_TOKEN }} 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - run: | 27 | echo "JAVA_HOME=$JAVA_HOME_21_X64" >> $GITHUB_ENV 28 | echo "$JAVA_HOME_21_X64/bin" >> $GITHUB_PATH 29 | 30 | - name: Setup Clojure 31 | uses: DeLaGuardo/setup-clojure@13.1 32 | with: 33 | cli: latest 34 | lein: latest 35 | 36 | - run: ./script/test.sh 37 | 38 | - if: ${{ startsWith(github.ref, 'refs/tags/') }} 39 | name: Set version 40 | run: | 41 | sed -i 's/"0.0.0"/"${{ github.ref_name }}"/g' project.clj 42 | 43 | - run: lein jar 44 | 45 | - if: ${{ startsWith(github.ref, 'refs/tags/') }} 46 | name: Deploy to Clojars 47 | run: | 48 | lein deploy clojars 49 | 50 | - uses: actions/upload-artifact@v4 51 | with: 52 | name: jar 53 | path: 'target/*.jar' 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .repl-port 2 | .nrepl-port 3 | hs_err_pid*.log 4 | .cpcache 5 | target 6 | .calva/ 7 | .lsp/ 8 | .portal/ 9 | .clj-kondo/.cache/ 10 | **/__pycache__/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.9.7 - May 21, 2025 2 | 3 | - Fixed defonce disappearing after double reload of a dependency #22 4 | 5 | # 0.9.6 - May 8, 2025 6 | 7 | - Fixed duplicate key exception on reading maps with both `::ns/k` and `:ns/k` #21 8 | 9 | # 0.9.5 - May 4, 2025 10 | 11 | - Throw IllegalStateException when running `unload` or `reload` before `init` #20 via @carloshernandez2 12 | 13 | # 0.9.4 - Mar 19, 2025 14 | 15 | - Oopsie 16 | 17 | # 0.9.3 - Mar 19, 2025 18 | 19 | - Added time to reload logging in :quiter mode, add final "Reloaded" to :verbose mode too 20 | 21 | # 0.9.2 - Mar 19, 2025 22 | 23 | - Log both beginning and end of reload in :quiter mode 24 | 25 | # 0.9.1 - Mar 18, 2025 26 | 27 | - Warn about and skip init calls that happen during reload 28 | 29 | # 0.9.0 - Feb 29, 2025 30 | 31 | - Don't re-parse files that didn't change on disk #18 32 | - Add :output :verbose | :quieter | :quiet #17 #19 via @velios 33 | 34 | # 0.8.0 - Feb 13, 2025 35 | 36 | - Add `CLJ_RELOAD_AUTO_INIT` env variable and `clj-reload.auto-init` system property to optionally disable init-less workflow #16 37 | 38 | # 0.7.1 - June 11, 2024 39 | 40 | - Omitting `:dirs` will use system classpath 41 | - Add `clj-reload.core/classpath-dirs` 42 | 43 | # 0.7.0 - May 4, 2024 44 | 45 | - [ BREAKING ] `:only` argument will force load unloaded namespaces, but will not reload unchanged ones 46 | - Added `find-namespaces` 47 | 48 | # 0.6.0 - May 3, 2024 49 | 50 | - Disabled parallel init/reload via lock #9 51 | 52 | # 0.5.0 - Apr 15, 2024 53 | 54 | - Added `:files` option for custom file patterns #8 via @danieroux 55 | - Extracted `core/*config*` from `core/*state` 56 | 57 | # 0.4.3 - Mar 21, 2024 58 | 59 | - Do not report self-reference as a cycle #6 60 | - Parse record ctor syntax #7 61 | 62 | # 0.4.2 - Mar 20, 2024 63 | 64 | - Speed up topo-sort #5 65 | 66 | # 0.4.1 - Mar 7, 2024 67 | 68 | - Fixed issues when adding/removing keeps 69 | 70 | # 0.4.0 - Mar 4, 2024 71 | 72 | - Added `unload` 73 | 74 | # 0.3.0 - Feb 29, 2024 75 | 76 | - Support passing regexp as `:only` option 77 | 78 | # 0.2.0 - Feb 23, 2024 79 | 80 | Support optional “init-less” workflow: 81 | 82 | - Initialize by default with all dirs on classpath 83 | - Support `:clj-reload/no-reload` and `:clj-reload/no-unload` meta on ns 84 | 85 | # 0.1.3 - Feb 21, 2024 86 | 87 | - Support namespaces defined in multiple files #3 88 | 89 | # 0.1.2 - Feb 20, 2024 90 | 91 | - Fixed parsing files with aliased keywords #2 92 | - Support `:as-alias` 93 | 94 | # 0.1.1 - Feb 18, 2024 95 | 96 | - Support keeping private defs #1 97 | - Throw on unsupported keep forms 98 | 99 | # 0.1.0 - Feb 17, 2024 100 | 101 | - Initial -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nikita Prokopov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./.extras/logo.png) 2 | 3 | Smarter way to reload Clojure code. Clj-Reload tracks namespace dependencies, unloads namespaces, and then loads them in the correct topological order. 4 | 5 | This is only about namespace dependencies within a single project. It has nothing to do with Leiningen, Maven, JAR files, or repositories. 6 | 7 | ## Dependency 8 | 9 | ```clojure 10 | io.github.tonsky/clj-reload {:mvn/version "0.9.7"} 11 | ``` 12 | 13 | ## The problem 14 | 15 | Do you love interactive development? Although Clojure is set up perfectly for that, evaluating buffers one at a time can only get you so far. 16 | 17 | Once you start dealing with the state, you get data dependencies, and with them, evaluation order starts to matter, and now you change one line but have to re-eval half of your application to see the change. 18 | 19 | But how do you know which half? 20 | 21 | ## The solution 22 | 23 | Clj-reload to the rescue! 24 | 25 | Clj-reload scans your source dir, figures out the dependencies, tracks file modification times, and when you are finally ready to reload, it carefully unloads and loads back only the namespaces that you touched and the ones that depend on those. In the correct dependency order, too. 26 | 27 | Let’s do a simple example. 28 | 29 | a.clj: 30 | 31 | ```clojure 32 | (ns a 33 | (:require b)) 34 | ``` 35 | 36 | b.clj: 37 | 38 | ```clojure 39 | (ns b 40 | (:require c)) 41 | ``` 42 | 43 | c.clj: 44 | 45 | ```clojure 46 | (ns c) 47 | ``` 48 | 49 | Imagine you change something in `b.clj` and want to see these changes in your current REPL. What do you do? 50 | 51 | If you call 52 | 53 | ```clojure 54 | (clj-reload.core/reload) 55 | ``` 56 | 57 | it will notice that 58 | 59 | - `b.clj` was changed, 60 | - `a.clj` depends on `b.clj`, 61 | - there’s `c.clj` but it doesn’t depend on `a.clj` or `b.clj` and wasn’t changed. 62 | 63 | Then the following will happen: 64 | 65 | ``` 66 | Unloading a 67 | Unloading b 68 | Loading b 69 | Loading a 70 | ``` 71 | 72 | So: 73 | 74 | - `c` wasn’t touched — no reason to, 75 | - `b` was reloaded because it was changed, 76 | - `a` was loaded _after_ the new version of `b` was in place. Any dependencies `a` had will now point to the new versions of `b`. 77 | 78 | That’s the core proposition of `clj-reload`. 79 | 80 | ## Usage 81 | 82 | Initialize: 83 | 84 | ```clojure 85 | (require '[clj-reload.core :as reload]) 86 | 87 | (reload/init 88 | {:dirs ["src" "dev" "test"]}) 89 | ``` 90 | 91 | `:dirs` are relative to the working directory. 92 | 93 | Use: 94 | 95 | ```clojure 96 | (reload/reload) 97 | ; => {:unloaded [a b c], :loaded [c b a]} 98 | ``` 99 | 100 | Works best if assigned to a shortcut in your editor. 101 | 102 | ## Usage: recovering from errors 103 | 104 | `reload` can be called multiple times. If reload fails, fix the error and call `reload` again. 105 | 106 | Alternatively, you can call `unload` which will unload all changed code and will not try loading it back. Fix the code and call `reload` again to load it back. 107 | 108 | ## Usage: Return value 109 | 110 | `reload` returns a map of namespaces that were reloaded: 111 | 112 | ```clojure 113 | {:unloaded [ ...] 114 | :loaded [ ...]} 115 | ``` 116 | 117 | By default, `reload` throws if it can’t load a namespace. You can change it to return exception instead: 118 | 119 | ```clojure 120 | (reload/reload {:throw false}) 121 | 122 | ; => {:unloaded [a b c] 123 | ; :loaded [c b] 124 | ; :failed b 125 | ; :exception } 126 | ``` 127 | 128 | ## Usage: Choose what to reload 129 | 130 | By default, clj-reload will only reload namespaces that were both: 131 | 132 | - Already loaded 133 | - Changed on disk 134 | 135 | If you pass `:only :loaded` option to `reload`, it will reload all currently loaded namespaces, no matter if they were changed or not. 136 | 137 | If you pass regexp to `:only`, clj-reload will search for matching namespaces and load them. This is useful, for example, if you want to find all tests namespaces: 138 | 139 | ``` 140 | (reload/reload {:only #".*\.-test"}) 141 | ``` 142 | 143 | This will reload all changed namespaces, then find and load all unloaded namespaces that match the pattern. 144 | 145 | Finally, if you pass `:only :all` option to `reload`, it will reload all namespaces it can find in the specified `:dirs`, no matter whether loaded or changed. 146 | 147 | ## Usage: Skipping reload 148 | 149 | Some namespaces contain state you always want to persist between reloads. E.g. running web-server, UI window, etc. To prevent these namespaces from reloading, add them to `:no-reload` during `init`: 150 | 151 | ```clojure 152 | (reload/init 153 | {:dirs ... 154 | :no-reload '#{user myapp.state ...}}) 155 | ``` 156 | 157 | Alternatively, if you want to never unload some namespace, but still reload it (e.g. it contains important state, but only in `defonce`-s), use `:no-unload`: 158 | 159 | ```clojure 160 | (reload/init 161 | {:dirs ... 162 | :no-unload '#{app.main ...}}) 163 | ``` 164 | 165 | `:no-reload` implies `:no-unload`. 166 | 167 | ## Usage: Unload hooks 168 | 169 | Sometimes your namespace contains stateful resource that requires proper shutdown before unloading. For example, if you have a running web server defined in a namespace and you unload that namespace, it will just keep running in the background. 170 | 171 | To work around that, define an unload hook: 172 | 173 | ```clojure 174 | (def my-server 175 | (server/start app {:port 8080})) 176 | 177 | (defn before-ns-unload [] 178 | (server/stop my-server)) 179 | ``` 180 | 181 | `before-ns-unload` is the default name for the unload hook. If a function with that name exists in a namespace, it will be called before unloading. 182 | 183 | You can change the name (or set it to `nil`) during `init`: 184 | 185 | ```clojure 186 | (reload/init 187 | {:dirs [...] 188 | :unload-hook 'my-unload}) 189 | ``` 190 | 191 | For symmetry, there’s also `:reload-hook 'after-ns-reload` that triggers after reload. 192 | 193 | ## Usage: Keeping vars between reloads 194 | 195 | One of the main innovations of `clj-reload` is that it can keep selected variables between reloads. 196 | 197 | To do so, just add `^:clj-reload/keep` to the form: 198 | 199 | ```clojure 200 | (ns test) 201 | 202 | (defonce x 203 | (rand-int 1000)) 204 | 205 | ^:clj-reload/keep 206 | (def y 207 | (rand-int 1000)) 208 | 209 | ^:clj-reload/keep 210 | (defrecord Z []) 211 | ``` 212 | 213 | and then reload: 214 | 215 | ```clojure 216 | (let [x test/x 217 | y test/y 218 | z (test/->Z)] 219 | 220 | (reload/reload) 221 | 222 | (let [x' test/x 223 | y' test/y 224 | z' (test/->Z)] 225 | (is (= x x')) 226 | (is (= y y')) 227 | (is (identical? (class z) (class z'))))) 228 | ``` 229 | 230 | Here’s how it works: 231 | 232 | - `defonce` works out of the box. No need to do anything. 233 | - `def`/`defn`/`deftype`/`defrecord`/`defprotocol` can be annotated with `^:clj-reload/keep` and can be persisted too. 234 | - Project-specific forms can be added by extending `clj-reload.core/keep-methods` multimethod. 235 | 236 | Why is this important? With `tools.namespace` you will structure your code in a way that will work with its reload implementation. For example, you’d probably move persistent state and protocols into separate namespaces, not because logic dictates it, but because reload library will not work otherwise. 237 | 238 | `clj-reload` allows you to structure the code the way business logic dictates it, without the need to adapt to developer workflow. 239 | 240 | Simply put: the fact that you use `clj-reload` during development does not spill into your production code. 241 | 242 | ## Usage: init-less workflow 243 | 244 | Sometimes it might be useful to integrate `clj-reload` into your system-wide profile or into tool like CIDER to be available in all your projects without explicitly adding it as a dependency. 245 | 246 | To support that, `clj-reload`: 247 | 248 | - Lets you skip `init`, in which case it’ll initialize with every directory it can find on classpath, 249 | - Supports `:clj-reload/no-reload` and `:clj-reload/no-unload` meta on namespace symbol, like this: 250 | 251 | ```clojure 252 | (ns ^:clj-reload/no-reload no-reload 253 | (:require ...)) 254 | ``` 255 | 256 | In that case, you can just call `clj-reload.core/reload` and it should work with default settings. 257 | 258 | To disable initial `init`, set environment variable: 259 | 260 | ```sh 261 | CLJ_RELOAD_AUTO_INIT=false clj -M -m ... 262 | ``` 263 | 264 | or system property: 265 | 266 | ```sh 267 | clj -J-Dclj-reload.auto-init=false -M -m ... 268 | ``` 269 | 270 | ## Usage: Reloading custom file types 271 | 272 | If you have custom file types, like `*.repl`, you can specify that `clj-reload` should scan and reload them too: 273 | 274 | ```clojure 275 | (reload/init 276 | {:dirs [...] 277 | :files #".*[.](clj|cljc|repl)"}) 278 | ``` 279 | 280 | ## Usage: Finding namespaces 281 | 282 | Sometimes you just want to know what namespaces are there. Since `clj-reload` does this work already anyways, you can do it, too: 283 | 284 | ```clojure 285 | (reload/find-namespaces #".*-test") 286 | ;; => #{lib.core-test lib.impl-test ...} 287 | ``` 288 | 289 | ## Usage: Controlling output 290 | 291 | In init, you can set how much `clj-reload` will log during reload. Verbose (default): 292 | 293 | ``` 294 | => (reload/init {:output :verbose}) 295 | => (reload/reload) 296 | 297 | Unloading clojure+.print-test 298 | Unloading clojure+.print 299 | Unloading clojure+.error 300 | Loading clojure+.error 301 | Loading clojure+.print 302 | Loading clojure+.print-test 303 | ``` 304 | 305 | Quieter: 306 | 307 | ``` 308 | => (reload/init {:output :quieter}) 309 | => (reload/reload) 310 | 311 | Reloaded 4 namespaces 312 | ``` 313 | 314 | Quiet (no output): 315 | 316 | ``` 317 | => (reload/init {:output :quiet}) 318 | => (reload/reload) 319 | ``` 320 | 321 | ## Comparison: Evaluating buffer 322 | 323 | The simplest way to reload Clojure code is just re-evaluating an entire buffer. 324 | 325 | It works for simple cases but fails to account for dependencies. If something depends on your buffer, it won’t see these changes. 326 | 327 | The second pitfall is removing/renaming vars or functions. If you had: 328 | 329 | ```clojure 330 | (def a 1) 331 | 332 | (def b (+ a 1)) 333 | ``` 334 | 335 | and then change it to just 336 | 337 | ```clojure 338 | (def b (+ a 1)) 339 | ``` 340 | 341 | it will still compile! New code is evaluated “on top” of the old one, without unloading the old one first. The definition of `a` will persist in the namespace and let `b` compile. 342 | 343 | It might be really hard to spot these errors during long development sessions. 344 | 345 | ## Comparison: `(require ... :reload-all)` 346 | 347 | Clojure has `:reload` and `:reload-all` options for `require`. They do track upstream dependencies, but that’s about it. 348 | 349 | In our original example, if we do 350 | 351 | ```clojure 352 | (require 'a :reload-all) 353 | ``` 354 | 355 | it will load both `b` and `c`. This is excessive (`b` or `c` might not have changed), doesn’t keep track of downstream dependencies (if we reload `b`, it will not trigger `a`, only `c`) and it also “evals on top”, same as with buffer eval. 356 | 357 | ## Comparison: tools.namespace 358 | 359 | [tools.namespace](https://github.com/clojure/tools.namespace) is a tool originally written by Stuart Sierra to work around the same problems. It’s a fantastic tool and the main inspiration for `clj-reload`. I’ve been using it for years and loving it, until I realized I wanted more. 360 | 361 | So the main proposition of both `tools.namespace` and `clj-reload` is the same: they will track file modification times and reload namespaces in the correct topological order. 362 | 363 | This is how `clj-reload` is different: 364 | 365 | - `tools.namespace` reloads every namespace it can find. `clj-reload` only reloads the ones that were already loaded. This allows you to have broken/experimental/auxiliary files lie around without breaking your workflow [TNS-65](https://clojure.atlassian.net/browse/TNS-65) 366 | 367 | - First reload in `tools.namespace` always reloads everything. In `clj-reload`, even the very first reload only reloads files that were actually changed [TNS-62](https://clojure.atlassian.net/browse/TNS-62) 368 | 369 | - `clj-reload` supports namespaces split across multiple files (like `core_deftype.clj`, `core_defprint.clj` in Clojure) [TNS-64](https://clojure.atlassian.net/browse/TNS-64) 370 | 371 | - `clj-reload` can see dependencies in top-level standalone `require` and `use` forms [TNS-64](https://clojure.atlassian.net/browse/TNS-64) 372 | 373 | - `clj-reload` supports load and unload hooks per namespace. Since `tools.namespace` doesn’t report which namespaces it’s going to reload, you always have to rebuild the entire state [TNS-63](https://clojure.atlassian.net/browse/TNS-63) 374 | 375 | - `clj-reload` can specify exclusions during configuration, without polluting the source code of those namespaces. 376 | 377 | - `clj-reload` can keep individual vars around and restore previous values after reload. E.g. `defonce` doesn’t really work with `tools.namespace`, but it does with `clj-reload`. 378 | 379 | - `clj-reload` has 2× smaller codebase and 0 runtime dependencies. 380 | 381 | - `clj-reload` doesn’t support ClojureScript. Patches welcome. 382 | 383 | ## A word of caution 384 | 385 | Clj-reload works by removing whole namespaces. Everything in them including vars is gone. This is different from how Clojure REPL usually works, in which calling `def` twice will just override its _value_, keeping the var itself the same. 386 | 387 | So if you store a link to a var somewhere, it’ll be pointing to the old version after reload. If you required/aliased a namespace, it’ll be pointing to the old version after reload. 388 | 389 | For your code to see new changes, use `(resolve 'full.ns/sym)` instead of just `full.ns/sym` or even `@#'full.ns/sym`, and do not put namespaces you’ll be reloading in `:require` portion of your REPL namespace. 390 | 391 | ## ClojureScript? 392 | 393 | `clj-reload` doesn’t support ClojureScript. Patches are welcome. 394 | 395 | ## License 396 | 397 | Copyright © 2024 Nikita Prokopov 398 | 399 | Licensed under [MIT](LICENSE). 400 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {org.clojure/clojure {:mvn/version "1.12.0"}} 3 | :aliases 4 | {:dev 5 | {:jvm-opts ["-ea" "-Duser.language=en" "-Duser.country=US" "-Dfile.encoding=UTF-8"] 6 | :extra-paths ["dev" "test" "fixtures/core_test" "fixtures/keep_test"] 7 | :extra-deps 8 | {org.clojure/tools.namespace {:mvn/version "1.5.0"}}}}} -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clojure.core.server :as server] 4 | [clojure.java.io :as io] 5 | [clojure.test :as test] 6 | [clojure.tools.namespace.repl :as ns] 7 | [clojure.tools.namespace.track :as track])) 8 | 9 | (ns/disable-reload!) 10 | 11 | (ns/set-refresh-dirs "src" "dev" "test") 12 | 13 | (defn reload 14 | ([] 15 | (reload nil)) 16 | ([opts] 17 | (set! *warn-on-reflection* true) 18 | (let [tracker (ns/scan opts) 19 | cnt (count (::track/load tracker)) 20 | res (apply ns/refresh-scanned (mapcat vec opts))] 21 | (when (instance? Throwable res) 22 | (throw res)) 23 | (str "Reloaded " cnt " namespace" (when (> cnt 1) "s"))))) 24 | 25 | (defn -main [& args] 26 | (alter-var-root #'*command-line-args* (constantly args)) 27 | (let [{port "--port"} args 28 | port (if (or (nil? port) (zero? port)) 29 | (+ 1024 (rand-int 64512)) 30 | (parse-long port))] 31 | (println "Started Server Socket REPL on port" port) 32 | (let [file (io/file ".repl-port")] 33 | (spit file port) 34 | (.deleteOnExit file)) 35 | (server/start-server 36 | {:name "repl" 37 | :accept 'clojure.core.server/repl 38 | :server-daemon false 39 | :port port}))) 40 | 41 | (defn test-all [] 42 | (reload) 43 | (test/run-all-tests #"clj-reload\..*-test")) 44 | 45 | (defn -test-main [_] 46 | (let [{:keys [fail error]} (test-all)] 47 | (System/exit (+ fail error)))) 48 | -------------------------------------------------------------------------------- /fixtures/core_test/a.clj: -------------------------------------------------------------------------------- 1 | (ns a 2 | (:require 3 | [b :as b] 4 | c 5 | d 6 | [z :as-alias z]) 7 | (:import 8 | [java.io File])) 9 | -------------------------------------------------------------------------------- /fixtures/core_test/b.clj: -------------------------------------------------------------------------------- 1 | (ns b) -------------------------------------------------------------------------------- /fixtures/core_test/c.clj: -------------------------------------------------------------------------------- 1 | (ns c 2 | (:require e)) -------------------------------------------------------------------------------- /fixtures/core_test/custom_file_type.repl: -------------------------------------------------------------------------------- 1 | (ns custom-file-type) -------------------------------------------------------------------------------- /fixtures/core_test/d.clj: -------------------------------------------------------------------------------- 1 | (ns d 2 | (:require e)) -------------------------------------------------------------------------------- /fixtures/core_test/double.clj: -------------------------------------------------------------------------------- 1 | (ns double 2 | (:require 3 | clojure.string)) 4 | 5 | (def a :a) 6 | -------------------------------------------------------------------------------- /fixtures/core_test/double_b.clj: -------------------------------------------------------------------------------- 1 | (ns double 2 | (:require 3 | clojure.set)) 4 | 5 | (def b :b) 6 | -------------------------------------------------------------------------------- /fixtures/core_test/e.clj: -------------------------------------------------------------------------------- 1 | (ns e) -------------------------------------------------------------------------------- /fixtures/core_test/err_parse.clj: -------------------------------------------------------------------------------- 1 | (ns err_parse) 2 | 3 | (abc -------------------------------------------------------------------------------- /fixtures/core_test/err_runtime.clj: -------------------------------------------------------------------------------- 1 | (ns err-runtime) 2 | 3 | (/ 1 0) -------------------------------------------------------------------------------- /fixtures/core_test/f.clj: -------------------------------------------------------------------------------- 1 | (ns f 2 | (:require d g)) -------------------------------------------------------------------------------- /fixtures/core_test/g.clj: -------------------------------------------------------------------------------- 1 | (ns g) -------------------------------------------------------------------------------- /fixtures/core_test/h.clj: -------------------------------------------------------------------------------- 1 | (ns h 2 | (:require e)) -------------------------------------------------------------------------------- /fixtures/core_test/i.clj: -------------------------------------------------------------------------------- 1 | (ns i 2 | (:require j)) 3 | -------------------------------------------------------------------------------- /fixtures/core_test/j.clj: -------------------------------------------------------------------------------- 1 | (ns j 2 | (:require k)) 3 | -------------------------------------------------------------------------------- /fixtures/core_test/k.clj: -------------------------------------------------------------------------------- 1 | (ns k) 2 | -------------------------------------------------------------------------------- /fixtures/core_test/l.clj: -------------------------------------------------------------------------------- 1 | (ns l) 2 | -------------------------------------------------------------------------------- /fixtures/core_test/m.clj: -------------------------------------------------------------------------------- 1 | (ns m 2 | (:require n o)) 3 | 4 | (defn before-ns-unload [] 5 | (swap! o/*atom conj :unload-m)) 6 | 7 | (defn after-ns-reload [] 8 | (swap! o/*atom conj :reload-m)) 9 | -------------------------------------------------------------------------------- /fixtures/core_test/n.clj: -------------------------------------------------------------------------------- 1 | (ns n 2 | (:require o)) 3 | 4 | (defn before-ns-unload [] 5 | (swap! o/*atom conj :unload-n)) 6 | 7 | (defn after-ns-reload [] 8 | (swap! o/*atom conj :reload-n)) 9 | -------------------------------------------------------------------------------- /fixtures/core_test/no_ns.clj: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /fixtures/core_test/no_reload.clj: -------------------------------------------------------------------------------- 1 | (ns ^:clj-reload/no-reload no-reload) 2 | 3 | (def rand1 4 | (rand-int Integer/MAX_VALUE)) -------------------------------------------------------------------------------- /fixtures/core_test/no_unload.clj: -------------------------------------------------------------------------------- 1 | (ns ^:clj-reload/no-unload no-unload) 2 | 3 | (def rand1 4 | (rand-int Integer/MAX_VALUE)) 5 | 6 | (def rand2 7 | (rand-int Integer/MAX_VALUE)) -------------------------------------------------------------------------------- /fixtures/core_test/o.clj: -------------------------------------------------------------------------------- 1 | (ns o) 2 | 3 | (def *atom 4 | (atom [])) -------------------------------------------------------------------------------- /fixtures/core_test/split.clj: -------------------------------------------------------------------------------- 1 | (ns split 2 | (:require 3 | [clojure.string :as str])) 4 | 5 | (load "split_part") 6 | -------------------------------------------------------------------------------- /fixtures/core_test/split_part.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'split) 2 | 3 | (require '[clojure.set :as set]) 4 | 5 | (def split-part 6 | 1) -------------------------------------------------------------------------------- /fixtures/core_test/two_nses.clj: -------------------------------------------------------------------------------- 1 | (ns two-nses 2 | (:require 3 | [clojure.string :as str])) 4 | 5 | (ns two-nses-second 6 | (:require 7 | [clojure.set :as set])) 8 | -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/dependency.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.dependency) -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/keep_custom.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-custom) 2 | 3 | (defmacro deftype+ [& body] 4 | `(deftype ~@body)) 5 | 6 | ^:clj-reload/keep 7 | (deftype+ CustomTypeKeep [t]) 8 | 9 | (def custom-type-keep 10 | (CustomTypeKeep. 0)) 11 | -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/keep_defprotocol.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-defprotocol) 2 | 3 | ^:clj-reload/keep 4 | (defprotocol IProto 5 | :extend-via-metadata true 6 | (-method [_])) 7 | 8 | ;; --- 9 | 10 | (defrecord RecInline [] 11 | IProto 12 | (-method [_] 13 | :rec-inline)) 14 | 15 | (def rec-inline 16 | (RecInline.)) 17 | 18 | ;; --- 19 | 20 | (defrecord RecExtendProto []) 21 | 22 | (def rec-extend-proto 23 | (RecExtendProto.)) 24 | 25 | (extend-protocol IProto 26 | RecExtendProto 27 | (-method [_] 28 | :rec-extend-proto)) 29 | 30 | ;; --- 31 | 32 | (defrecord RecExtendType []) 33 | 34 | (def rec-extend-type 35 | (RecExtendType.)) 36 | 37 | (extend-type RecExtendType 38 | IProto 39 | (-method [_] 40 | :rec-extend-type)) 41 | 42 | ;; --- 43 | 44 | (defrecord RecExtend []) 45 | 46 | (def rec-extend 47 | (RecExtend.)) 48 | 49 | (extend RecExtend 50 | IProto 51 | {:-method (fn [_] 52 | :rec-extend)}) 53 | 54 | ;; --- 55 | 56 | (def extend-meta 57 | ^{'clj-reload.keep-defprotocol/-method 58 | (fn [_] 59 | :extend-meta)} []) 60 | -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/keep_defrecord.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-defrecord) 2 | 3 | (defrecord RecordNormal [t]) 4 | 5 | (def record-normal-new 6 | (RecordNormal. 0)) 7 | 8 | (def record-normal-factory 9 | (->RecordNormal 0)) 10 | 11 | (def record-normal-map-factory 12 | (map->RecordNormal {:t 0})) 13 | 14 | ^:clj-reload/keep 15 | (defrecord RecordKeep [t]) 16 | 17 | (def record-keep-new 18 | (RecordKeep. 0)) 19 | 20 | (def record-keep-factory 21 | (->RecordKeep 0)) 22 | 23 | (def record-keep-map-factory 24 | (map->RecordKeep {:t 0})) 25 | -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/keep_deftype.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-deftype) 2 | 3 | (deftype TypeNormal [t] 4 | java.lang.Object 5 | (equals [_ o] 6 | (and (instance? TypeNormal o) 7 | (= (.-t ^TypeNormal o) t)))) 8 | 9 | (def type-normal-new 10 | (TypeNormal. 0)) 11 | 12 | (def type-normal-factory 13 | (->TypeNormal 0)) 14 | 15 | ^:clj-reload/keep 16 | (deftype TypeKeep [t] 17 | java.lang.Object 18 | (equals [_ o] 19 | (and (instance? TypeKeep o) 20 | (= (.-t ^TypeKeep o) t)))) 21 | 22 | (def type-keep-new 23 | (TypeKeep. 0)) 24 | 25 | (def type-keep-factory 26 | (->TypeKeep 0)) 27 | -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/keep_downstream.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-downstream 2 | (:require 3 | clj-reload.keep-upstream)) 4 | 5 | (defonce downstream-var 6 | (rand-int Integer/MAX_VALUE)) -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/keep_unsupported.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-unsupported) 2 | 3 | (defmacro defcomp [& body] 4 | `(def ~@body)) 5 | 6 | ^:clj-reload/keep 7 | (defcomp v 1) 8 | -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/keep_upstream.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-upstream) 2 | 3 | (defonce upstream-var 4 | (rand-int Integer/MAX_VALUE)) 5 | -------------------------------------------------------------------------------- /fixtures/keep_test/clj_reload/keep_vars.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-vars 2 | (:require 3 | [clj-reload.dependency])) 4 | 5 | (def normal 6 | (rand-int Integer/MAX_VALUE)) 7 | 8 | (defonce *atom 9 | (atom nil)) 10 | 11 | ^:clj-reload/keep 12 | (def just-var 13 | (rand-int Integer/MAX_VALUE)) 14 | 15 | (def ^:clj-reload/keep just-var-2 16 | (rand-int Integer/MAX_VALUE)) 17 | 18 | ^:clj-reload/keep 19 | (def ^:private private-var 20 | (rand-int Integer/MAX_VALUE)) 21 | 22 | (def dependent 23 | [just-var (rand-int Integer/MAX_VALUE)]) 24 | 25 | ^:clj-reload/keep 26 | (def ^{:k :v} meta-var 27 | (rand-int Integer/MAX_VALUE)) 28 | 29 | ^:clj-reload/keep 30 | (defn public-fn [a]) 31 | 32 | ^:clj-reload/keep 33 | (defn- ^String private-fn [a b c]) 34 | 35 | (def normal-2 36 | (rand-int Integer/MAX_VALUE)) 37 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject io.github.tonsky/clj-reload "0.0.0" 2 | :description "Smarter way to reload Clojure code" 3 | :license {:name "MIT" :url "https://github.com/tonsky/clj-reload/blob/master/LICENSE"} 4 | :url "https://github.com/tonsky/clj-reload" 5 | :dependencies 6 | [[org.clojure/clojure "1.11.1"]] 7 | :deploy-repositories 8 | {"clojars" 9 | {:url "https://clojars.org/repo" 10 | :username "tonsky" 11 | :password :env/clojars_token 12 | :sign-releases false}}) -------------------------------------------------------------------------------- /script/repl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "`dirname $0`/.." 4 | 5 | clojure -M:dev -m user -------------------------------------------------------------------------------- /script/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o nounset -o pipefail 3 | cd "`dirname $0`/.." 4 | 5 | clojure -X:dev user/-test-main -------------------------------------------------------------------------------- /src/clj_reload/core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.core 2 | (:require 3 | [clj-reload.keep :as keep] 4 | [clj-reload.parse :as parse] 5 | [clj-reload.util :as util] 6 | [clojure.java.io :as io]) 7 | (:import 8 | [java.util.concurrent.locks ReentrantLock])) 9 | 10 | ; Config :: {:dirs [ ...] - where to look for files 11 | ; :files #"" - which files to scan, defaults to #".*\.cljc?" 12 | ; :no-unload #{ ...} - list of nses to skip unload 13 | ; :no-reload #{ ...} - list of nses to skip reload 14 | ; :reload-hook - if function with this name exists, 15 | ; it will be called after reloading. 16 | ; default: after-ns-reload 17 | ; :unload-hook - if function with this name exists, 18 | ; it will be called before unloading. 19 | ; default: before-ns-unload 20 | ; :output } - verbosity of log output. Options: 21 | ; :verbose - print Unloading/Reloading for each namespace 22 | ; :quieter - only print 'Reloaded N namespaces' 23 | ; :quiet - no output at all 24 | ; default: :verbose 25 | 26 | (def ^:private ^:dynamic *config*) 27 | 28 | ; State :: {:since - last time list of files was scanned 29 | ; :files { -> File} - all found files 30 | ; :namespaces { -> NS} - all found namespaces 31 | ; :to-unload #{ ...} - list of nses pending unload 32 | ; :to-load #{ ...}} - list of nses pending load 33 | ; 34 | ; File :: {:namespaces #{ ...} - nses defined in this file 35 | ; :modified } - lastModified 36 | ; 37 | ; NS :: {:ns-files #{ ...} - files containing (ns ...) declaration 38 | ; :in-ns-files #{ ...} - files containing (in-ns ...) declaration 39 | ; :requires #{ ...} - other nses this depends on 40 | ; :meta {} - metadata from ns symbol 41 | ; :keep { -> Keep}}} - vars to keep between reloads 42 | ; 43 | ; Keep :: {:tag - type of value ('def, 'defonce etc) 44 | ; :form - full source form, just in case 45 | ; 46 | ; // stashed vars - one or more of these will contain 47 | ; values remembered between reloads 48 | ; :var Var? 49 | ; :ctor Var? 50 | ; :map-ctor Var? 51 | ; :proto Var? 52 | ; :methods { Var}?} 53 | 54 | (def ^:private *state 55 | (atom {})) 56 | 57 | (def ^ReentrantLock lock 58 | (ReentrantLock.)) 59 | 60 | (defmacro with-lock [& body] 61 | `(try 62 | (.lock lock) 63 | ~@body 64 | (finally 65 | (.unlock lock)))) 66 | 67 | (defn- files->namespaces [files already-read] 68 | (let [*res (volatile! {})] 69 | (doseq [file files 70 | [name namespace] (or 71 | (already-read file) 72 | (parse/read-file file)) 73 | :when (not (util/throwable? namespace))] 74 | (vswap! *res update name #(merge-with into % namespace))) 75 | @*res)) 76 | 77 | (defn- scan-impl [{files-before :files 78 | nses-before :namespaces} since] 79 | (let [files-now (->> (:dirs *config*) 80 | (mapcat #(file-seq (io/file %))) 81 | (filter util/file?) 82 | (filter #(re-matches (:files *config*) (util/file-name %)))) 83 | 84 | [files-modified 85 | files-broken] (reduce 86 | (fn [[modified broken] file] 87 | (if (<= (util/last-modified file) since) 88 | [modified broken] 89 | (let [res (parse/read-file file)] 90 | (if (util/throwable? res) 91 | [modified (assoc broken file res)] 92 | [(assoc modified file res) broken])))) 93 | [{} {}] files-now) 94 | 95 | files-deleted (reduce disj (set (keys files-before)) files-now) 96 | 97 | nses-broken (util/for-map [[file ex] files-broken 98 | ns (get-in files-before [file :namespaces])] 99 | [ns ex]) 100 | 101 | nses-unload (reduce 102 | #(into %1 (get-in files-before [%2 :namespaces])) 103 | #{} 104 | (concat (keys files-modified) files-deleted)) 105 | 106 | nses-load (util/for-set [[file namespaces] files-modified 107 | ns (keys namespaces)] 108 | ns) 109 | 110 | files' (as-> files-before % 111 | (reduce dissoc % files-deleted) 112 | (merge % 113 | (util/for-map [[file namespaces] files-modified] 114 | [file {:namespaces (set (keys namespaces)) 115 | :modified (util/last-modified file)}]))) 116 | 117 | already-read (merge 118 | (util/for-map [[file {:keys [namespaces]}] files-before] 119 | [file (select-keys nses-before namespaces)]) 120 | files-modified 121 | (util/for-map [[file _] files-broken] 122 | [file {}])) 123 | 124 | nses' (files->namespaces (keys files') already-read)] 125 | 126 | {:broken nses-broken 127 | :files' files' 128 | :namespaces' nses' 129 | :to-unload' nses-unload 130 | :to-load' nses-load})) 131 | 132 | (defn find-namespaces 133 | "Returns namespaces matching regex, or all of them" 134 | ([] 135 | (find-namespaces #".*")) 136 | ([regex] 137 | (binding [util/*log-fn* nil] 138 | (let [{:keys [namespaces']} (scan-impl @*state 0)] 139 | (into #{} (filter #(re-matches regex (name %)) (keys namespaces'))))))) 140 | 141 | (def ^{:doc "Returns dirs that are currently on classpath" 142 | :arglists '([])} 143 | classpath-dirs 144 | util/classpath-dirs) 145 | 146 | (defn init 147 | "Options: 148 | 149 | :dirs :: [ ...] - where to look for files 150 | :files :: #\"\" - which files to scan, defaults to #\".*\\\\.cljc?\" 151 | :no-reload :: #{ ...} - list of namespaces to skip reload entirely 152 | :no-unload :: #{ ...} - list of namespaces to skip unload only. 153 | These will be loaded “on top” of previous state 154 | :unload-hook :: - if function with this name exists in a namespace, 155 | it will be called before unloading. Default: 'before-ns-unload 156 | :reload-hook :: - if function with this name exists in a namespace, 157 | it will be called after reloading. Default: 'after-ns-reload 158 | :output :: - verbosity of log output. Options: 159 | :verbose - print Unloading/Reloading for each namespace 160 | :quieter - only print 'Reloaded N namespaces' 161 | :quiet - no output at all 162 | Default: :verbose" 163 | [opts] 164 | (if (.isHeldByCurrentThread lock) 165 | (util/log "Called `init` from inside `reload`, skipping") 166 | (with-lock 167 | (binding [util/*log-fn* nil] 168 | (let [dirs (vec (or (:dirs opts) (classpath-dirs))) 169 | files (or (:files opts) #".*\.cljc?") 170 | now (util/now)] 171 | (alter-var-root #'*config* 172 | (constantly 173 | {:dirs dirs 174 | :files files 175 | :no-unload (set (:no-unload opts)) 176 | :no-reload (set (:no-reload opts)) 177 | :reload-hook (:reload-hook opts 'after-ns-reload) 178 | :unload-hook (:unload-hook opts 'before-ns-unload) 179 | :output (:output opts :verbose)})) 180 | (let [{:keys [files' namespaces']} (scan-impl nil 0)] 181 | (reset! *state {:since now 182 | :files files' 183 | :namespaces namespaces'}))))))) 184 | 185 | (defn- topo-sort-fn 186 | "Accepts dependees map {ns -> #{downsteram-ns ...}}, 187 | returns a fn that topologically sorts dependencies" 188 | [deps] 189 | (let [sorted (parse/topo-sort deps)] 190 | (fn [coll] 191 | (filter (set coll) sorted)))) 192 | 193 | (defn- add-unloaded [scan re loaded] 194 | (let [new (->> (keys (:namespaces' scan)) 195 | (remove loaded) 196 | (filter #(re-matches re (str %))))] 197 | (update scan :to-load' into new))) 198 | 199 | (defn carry-keeps [from to] 200 | (util/for-map [[ns-sym ns] to] 201 | [ns-sym (assoc ns :keep 202 | (merge-with merge 203 | (get-in from [ns-sym :keep]) 204 | (:keep ns)))])) 205 | 206 | (defn- scan [state opts] 207 | (let [{:keys [no-unload no-reload]} *config* 208 | {:keys [since to-load to-unload files namespaces]} state 209 | {:keys [only] :or {only :changed}} opts 210 | now (util/now) 211 | loaded @@#'clojure.core/*loaded-libs* 212 | {:keys [broken 213 | files' 214 | namespaces' 215 | to-unload' 216 | to-load']} (case only 217 | :changed (scan-impl state since) 218 | :loaded (scan-impl state 0) 219 | :all (scan-impl state 0) 220 | #_regex (-> (scan-impl state since) 221 | (add-unloaded only loaded))) 222 | 223 | _ (doseq [[ns {:keys [exception]}] broken 224 | :when (loaded ns) 225 | :when (not (no-reload ns))] 226 | (throw exception)) 227 | 228 | since' (transduce (map :modified) max (max since now) (vals files')) 229 | 230 | unload? #(and 231 | (loaded %) 232 | (not (:clj-reload/no-unload (:meta (namespaces %)))) 233 | (not (:clj-reload/no-reload (:meta (namespaces %)))) 234 | (not (no-unload %)) 235 | (not (no-reload %))) 236 | deps (parse/dependees namespaces) 237 | topo-sort (topo-sort-fn deps) 238 | to-unload'' (->> to-unload' 239 | (filter unload?) 240 | (parse/transitive-closure deps) 241 | (filter unload?) 242 | (concat to-unload) 243 | (topo-sort) 244 | (reverse)) 245 | 246 | load? #(and 247 | (case only 248 | :changed (loaded %) 249 | :loaded (loaded %) 250 | :all true 251 | #_regex (or (loaded %) (re-matches only (str %)))) 252 | (not (:clj-reload/no-reload (:meta (namespaces %)))) 253 | (not (no-reload %)) 254 | (namespaces' %)) 255 | deps' (parse/dependees namespaces') 256 | topo-sort' (topo-sort-fn deps') 257 | to-load'' (->> to-load' 258 | (filter load?) 259 | (parse/transitive-closure deps') 260 | (filter load?) 261 | (concat to-load) 262 | (topo-sort'))] 263 | (assoc state 264 | :since since' 265 | :files files' 266 | :namespaces (carry-keeps namespaces namespaces') 267 | :to-unload to-unload'' 268 | :to-load to-load''))) 269 | 270 | (defn- ns-unload [ns] 271 | (when (= (:output *config*) :verbose) 272 | (util/log "Unloading" ns)) 273 | (try 274 | (when-some [unload-hook (:unload-hook *config*)] 275 | (when-some [ns-obj (find-ns ns)] 276 | (when-some [unload-fn (ns-resolve ns-obj unload-hook)] 277 | (unload-fn)))) 278 | (catch Throwable t 279 | ;; eat up unload error 280 | ;; if we can’t unload there’s no way to fix that 281 | ;; because any changes would require reload first 282 | (util/log " exception during unload hook" t))) 283 | (remove-ns ns) 284 | (dosync 285 | (alter @#'clojure.core/*loaded-libs* disj ns))) 286 | 287 | (defn- ns-load [ns file keeps] 288 | (when (= (:output *config*) :verbose) 289 | (util/log "Loading" ns #_"from" #_(util/file-path file))) 290 | (try 291 | (if (empty? keeps) 292 | (util/ns-load-file (slurp file) ns file) 293 | (keep/ns-load-patched ns file keeps)) 294 | 295 | (when-some [reload-hook (:reload-hook *config*)] 296 | (when-some [reload-fn (ns-resolve (find-ns ns) reload-hook)] 297 | (reload-fn))) 298 | 299 | nil 300 | (catch Throwable t 301 | (util/log " failed to load" ns t) 302 | t))) 303 | 304 | (defn unload 305 | "Same as `reload`, but does not loads namespaces back" 306 | ([] 307 | (unload nil)) 308 | ([opts] 309 | (with-lock 310 | (when (empty? *config*) 311 | (throw (IllegalStateException. "clj-reload not initialized. Call `init` first"))) 312 | (binding [util/*log-fn* (:log-fn opts util/*log-fn*)] 313 | (swap! *state scan opts) 314 | (when (= (:output *config*) :quieter) 315 | (util/log (format "Reloading %s namespaces..." (count (:to-load @*state))))) 316 | (loop [unloaded []] 317 | (let [state @*state] 318 | (if (not-empty (:to-unload state)) 319 | (let [[ns & to-unload'] (:to-unload state) 320 | keeps (keep/resolve-keeps ns (-> state :namespaces ns :keep))] 321 | (ns-unload ns) 322 | (swap! *state 323 | #(-> % 324 | (assoc :to-unload to-unload') 325 | (update :namespaces update ns update :keep util/deep-merge keeps))) 326 | (recur (conj unloaded ns))) 327 | (do 328 | (when (and 329 | (= (:output *config*) :verbose) 330 | (empty? unloaded)) 331 | (util/log "Nothing to unload")) 332 | {:unloaded unloaded})))))))) 333 | 334 | (defn reload 335 | "Options: 336 | 337 | :throw :: true | false - throw or return exception, default true 338 | :log-fn :: (fn [& args]) - fn to display unload/reload status 339 | :only :: :changed - default. Only reloads changed already loaded files 340 | | :loaded - Reload all loaded files 341 | | - Reload all nses matching this pattern 342 | | :all - Reload everything it can find in dirs 343 | 344 | Returns map of what was reloaded 345 | 346 | {:unloaded [ ...] 347 | :loaded [ ...]} 348 | 349 | If anything fails, throws. If :throw false, return value will also have keys 350 | 351 | {:failed 352 | :exception } 353 | 354 | Can be called multiple times. If reload fails, fix the error and call `reload` again" 355 | ([] 356 | (reload nil)) 357 | ([opts] 358 | (with-lock 359 | (binding [util/*log-fn* (:log-fn opts util/*log-fn*)] 360 | (let [t (System/currentTimeMillis) 361 | {:keys [unloaded]} (unload opts)] 362 | (loop [loaded []] 363 | (let [state @*state] 364 | (if (not-empty (:to-load state)) 365 | (let [[ns & to-load'] (:to-load state) 366 | files (-> state :namespaces ns :ns-files)] 367 | (if-some [ex (some #(ns-load ns % (-> state :namespaces ns :keep)) files)] 368 | (do 369 | (swap! *state update :to-unload #(cons ns %)) 370 | (if (:throw opts true) 371 | (throw 372 | (ex-info 373 | (str "Failed to load namespace: " ns) 374 | {:unloaded unloaded 375 | :loaded loaded 376 | :failed ns} 377 | ex)) 378 | {:unloaded unloaded 379 | :loaded loaded 380 | :failed ns 381 | :exception ex})) 382 | (do 383 | (swap! *state #(-> % 384 | (assoc :to-load to-load') 385 | (update-in [:namespaces ns :keep] util/map-vals 386 | (fn [keep] 387 | (select-keys keep [:tag :form]))))) 388 | (recur (conj loaded ns))))) 389 | (do 390 | (when (and 391 | (= (:output *config*) :verbose) 392 | (empty? loaded)) 393 | (util/log "Nothing to reload")) 394 | (when (#{:verbose :quieter} (:output *config*)) 395 | (util/log (format "Reloaded %s namespaces in %s ms" (count loaded) (- (System/currentTimeMillis) t)))) 396 | {:unloaded unloaded 397 | :loaded loaded}))))))))) 398 | 399 | (defmulti keep-methods 400 | (fn [tag] 401 | tag)) 402 | 403 | (defmethod keep-methods :default [tag] 404 | (throw 405 | (ex-info 406 | (str "Keeping " tag " forms is not implemented") 407 | {:tag tag}))) 408 | 409 | (defmethod keep-methods 'def [_] 410 | keep/keep-methods-defs) 411 | 412 | (defmethod keep-methods 'defn [_] 413 | keep/keep-methods-defs) 414 | 415 | (defmethod keep-methods 'defn- [_] 416 | keep/keep-methods-defs) 417 | 418 | (defmethod keep-methods 'defonce [_] 419 | keep/keep-methods-defs) 420 | 421 | (defmethod keep-methods 'deftype [_] 422 | keep/keep-methods-deftype) 423 | 424 | (defmethod keep-methods 'defrecord [_] 425 | keep/keep-methods-defrecord) 426 | 427 | (defmethod keep-methods 'defprotocol [_] 428 | keep/keep-methods-defprotocol) 429 | 430 | ;; Initialize with classpath-dirs to support “init-less” workflow 431 | ;; See https://github.com/tonsky/clj-reload/pull/4 432 | ;; and https://github.com/clojure-emacs/cider-nrepl/issues/849 433 | (when (and 434 | (not= "false" (System/getenv "CLJ_RELOAD_AUTO_INIT")) 435 | (not= "false" (System/getProperty "clj-reload.auto-init"))) 436 | (init 437 | {:dirs (classpath-dirs)})) 438 | -------------------------------------------------------------------------------- /src/clj_reload/keep.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep 2 | (:require 3 | [clj-reload.util :as util] 4 | [clojure.string :as str]) 5 | (:import 6 | [clojure.lang Var] 7 | [java.io File])) 8 | 9 | (defn maybe-quote [form] 10 | (cond 11 | (symbol? form) 12 | (list 'quote form) 13 | 14 | (and (sequential? form) (not (vector? form))) 15 | (list 'quote form) 16 | 17 | :else 18 | form)) 19 | 20 | (defn meta-str [var] 21 | (let [meta (dissoc (meta var) :ns :file :line :column :name)] 22 | (when-not (empty? meta) 23 | (str "^" (pr-str (util/map-vals meta maybe-quote)) " ")))) 24 | 25 | (defn classname [ns sym] 26 | (-> (name ns) 27 | (str/replace "-" "_") 28 | (str "." sym))) 29 | 30 | (defn stash-ns [] 31 | (or 32 | (find-ns 'clj-reload.stash) 33 | (binding [*ns* *ns*] 34 | (in-ns 'clj-reload.stash) 35 | *ns*))) 36 | 37 | (def keep-methods-defs 38 | {:resolve 39 | (fn [ns sym keep] 40 | (when-some [var (resolve (symbol (name ns) (name sym)))] 41 | {:var var})) 42 | 43 | :patch 44 | (fn [ns sym keep] 45 | (when-some [var (:var keep)] 46 | (intern (stash-ns) sym @var) 47 | (str 48 | "(def " (meta-str var) sym " @#'clj-reload.stash/" sym ")")))}) 49 | 50 | (def keep-methods-deftype 51 | {:resolve 52 | (fn [ns sym keep] 53 | (when-some [ctor (resolve (symbol (name ns) (str "->" sym)))] 54 | {:ctor ctor})) 55 | 56 | :patch 57 | (fn [ns sym keep] 58 | (when-some [ctor (:ctor keep)] 59 | (intern (stash-ns) (symbol (str "->" sym)) @ctor) 60 | (str 61 | "(clojure.core/import " (classname ns sym) ") " 62 | "(def " (meta-str ctor) "->" sym " clj-reload.stash/->" sym ")")))}) 63 | 64 | (def keep-methods-defrecord 65 | {:resolve 66 | (fn [ns sym keep] 67 | (when-some [ctor (resolve (symbol (name ns) (str "->" sym)))] 68 | (when-some [map-ctor (resolve (symbol (name ns) (str "map->" sym)))] 69 | {:ctor ctor 70 | :map-ctor map-ctor}))) 71 | 72 | :patch 73 | (fn [ns sym keep] 74 | (when-some [ctor (:ctor keep)] 75 | (when-some [map-ctor (:map-ctor keep)] 76 | (intern (stash-ns) (symbol (str "->" sym)) @ctor) 77 | (intern (stash-ns) (symbol (str "map->" sym)) @map-ctor) 78 | (str 79 | "(clojure.core/import " (classname ns sym) ") " 80 | "(def " (meta-str ctor) "->" sym " clj-reload.stash/->" sym ") " 81 | "(def " (meta-str map-ctor) "map->" sym " clj-reload.stash/map->" sym ")"))))}) 82 | 83 | (defn update-protocol-method-builders [proto & vars] 84 | (let [mb (:method-builders proto) 85 | vars (util/for-map [var vars] 86 | [(symbol var) var]) 87 | mb' (util/for-map [[var val] mb] 88 | [(vars (symbol var)) val])] 89 | (alter-var-root (:var proto) assoc :method-builders mb'))) 90 | 91 | (def keep-methods-defprotocol 92 | {:resolve 93 | (fn [ns sym keep] 94 | (when-some [proto (resolve (symbol (name ns) (name sym)))] 95 | {:proto proto 96 | :methods (util/for-map [[method-var _] (:method-builders @proto)] 97 | [(.-sym ^Var method-var) method-var])})) 98 | 99 | :patch 100 | (fn [ns sym keep] 101 | (when-some [proto (:proto keep)] 102 | (when-some [methods (:methods keep)] 103 | (intern (stash-ns) sym @proto) 104 | (doseq [[method-sym method] methods] 105 | (intern (find-ns 'clj-reload.stash) method-sym @method)) 106 | (str 107 | "(def " (meta-str proto) sym " clj-reload.stash/" sym ") " 108 | "(clojure.core/alter-var-root #'" sym " assoc :var #'" sym ") " 109 | (str/join " " 110 | (for [[method-sym method] methods] 111 | (str "(def " (meta-str method) "^{:protocol #'" sym "} " method-sym " clj-reload.stash/" method-sym ")"))) 112 | " (clj-reload.keep/update-protocol-method-builders " sym " " 113 | (str/join " " 114 | (for [[method-sym _] methods] 115 | (str "#'" method-sym))) ") " 116 | ; "(-reset-methods " sym ")" 117 | ))))}) 118 | 119 | (def keep-methods 120 | (delay 121 | @(resolve 'clj-reload.core/keep-methods))) 122 | 123 | (defn keep-resolve [ns sym keep] 124 | ((:resolve (@keep-methods (:tag keep))) ns sym keep)) 125 | 126 | (defn keep-patch [ns sym keep] 127 | ((:patch (@keep-methods (:tag keep))) ns sym keep)) 128 | 129 | (defn resolve-keeps [ns syms] 130 | (util/for-map [[sym keep] syms 131 | :let [resolved (keep-resolve ns sym keep)] 132 | :when resolved] 133 | [sym resolved])) 134 | 135 | (defn patch-file [content patch-fn] 136 | (let [rdr (util/string-reader content) 137 | patched (StringBuilder.)] 138 | (loop [] 139 | (.captureString rdr) 140 | (let [form (util/read-form rdr) 141 | text (.getString rdr)] 142 | (cond 143 | (= :clj-reload.util/eof form) 144 | (str patched) 145 | 146 | (and (list? form) (>= (count form) 2)) 147 | (if-some [text' (patch-fn (take 2 form))] 148 | (let [[_ ws] (re-matches #"(?s)(\s*).*" text) 149 | _ (.append patched ws) 150 | text (subs text (count ws)) 151 | lines (str/split-lines text) 152 | text'' (if (= 1 (count lines)) 153 | (if (<= (count text) (count text')) 154 | text' 155 | (str text' (str/join (repeat (- (count text) (count text')) \space)))) 156 | (str text' 157 | (str/join (repeat (dec (count lines)) \newline)) 158 | (str/join (repeat (count (last lines)) \space))))] 159 | (do 160 | (.append patched text'') 161 | (recur))) 162 | (do 163 | (.append patched text) 164 | (recur))) 165 | 166 | :else 167 | (do 168 | (.append patched text) 169 | (recur))))))) 170 | 171 | (defn patch-fn [ns keeps] 172 | (fn [[tag sym]] 173 | (when-some [keep (get keeps sym)] 174 | (when (= tag (:tag keep)) 175 | (keep-patch ns sym keep))))) 176 | 177 | (defn ns-load-patched [ns ^File file keeps] 178 | (try 179 | (let [content (patch-file (slurp file) (patch-fn ns keeps))] 180 | (util/ns-load-file content ns file)) 181 | 182 | ;; check 183 | (@#'clojure.core/throw-if (not (find-ns ns)) 184 | "namespace '%s' not found after loading '%s'" 185 | ns (.getPath file)) 186 | 187 | (finally 188 | ;; drop everything in stash 189 | (remove-ns 'clj-reload.stash) 190 | (dosync 191 | (alter @#'clojure.core/*loaded-libs* disj 'clj-reload.stash))))) 192 | -------------------------------------------------------------------------------- /src/clj_reload/parse.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.parse 2 | (:require 3 | [clj-reload.util :as util] 4 | [clojure.string :as str] 5 | [clojure.walk :as walk]) 6 | (:import 7 | [java.io File])) 8 | 9 | (defn expand-quotes [form] 10 | (walk/postwalk 11 | #(if (and (sequential? %) (not (vector? %)) (= 'quote (first %))) 12 | (second %) 13 | %) 14 | form)) 15 | 16 | (defn parse-require-form [form] 17 | (loop [body (next form) 18 | result (transient #{})] 19 | (let [[decl & body'] body] 20 | (cond 21 | (empty? body) 22 | (persistent! result) 23 | 24 | (symbol? decl) ;; a.b.c 25 | (recur body' (conj! result decl)) 26 | 27 | (not (sequential? decl)) 28 | (do 29 | (util/log "Unexpected" (first form) "form:" (pr-str decl)) 30 | (recur body' result)) 31 | 32 | (not (symbol? (first decl))) 33 | (do 34 | (util/log "Unexpected" (first form) "form:" (pr-str decl)) 35 | (recur body' result)) 36 | 37 | (or 38 | (nil? (second decl)) ;; [a.b.d] 39 | (keyword? (second decl))) ;; [a.b.e :as e] 40 | (if (= :as-alias (second decl)) ;; [a.b.e :as-alias e] 41 | (recur body' result) 42 | (recur body' (conj! result (first decl)))) 43 | 44 | :else ;; [a.b f [g :as g]] 45 | (let [prefix (first decl) 46 | symbols (->> (next decl) 47 | (remove #(and (sequential? %) (= :as-alias (second %)))) ;; [a.b [g :as-alias g]] 48 | (map #(if (symbol? %) % (first %))) 49 | (map #(symbol (str (name prefix) "." (name %)))))] 50 | (recur body' (reduce conj! result symbols))))))) 51 | 52 | (defn parse-ns-form [form] 53 | (let [name (second form)] 54 | (loop [body (nnext form) 55 | requires (transient #{})] 56 | (let [[form & body'] body 57 | tag (when (list? form) 58 | (first form))] 59 | (cond 60 | (empty? body) 61 | [name (not-empty (persistent! requires))] 62 | 63 | (#{:require :use} tag) 64 | (recur body' (reduce conj! requires (parse-require-form form))) 65 | 66 | :else 67 | (recur body' requires)))))) 68 | 69 | (defn read-file 70 | "Returns { NS} or Exception" 71 | ([file] 72 | (with-open [rdr (util/file-reader file)] 73 | (try 74 | (read-file rdr file) 75 | (catch Exception e 76 | (util/log "Failed to read" (.getPath ^File file) (.getMessage e)) 77 | (ex-info (str "Failed to read " (.getPath ^File file)) {:file file} e))))) 78 | ([rdr file] 79 | (loop [ns nil 80 | nses {}] 81 | (let [form (util/read-form rdr) 82 | tag (when (list? form) 83 | (first form))] 84 | (cond 85 | (= :clj-reload.util/eof form) 86 | nses 87 | 88 | (= 'ns tag) 89 | (let [[ns requires] (parse-ns-form form) 90 | requires (disj requires ns)] 91 | (recur ns (update nses ns util/assoc-some 92 | :meta (meta ns) 93 | :requires requires 94 | :ns-files (util/some-set file)))) 95 | 96 | (= 'in-ns tag) 97 | (let [[_ ns] (expand-quotes form)] 98 | (recur ns (update nses ns util/assoc-some 99 | :in-ns-files (util/some-set file)))) 100 | 101 | (and (nil? ns) (#{'require 'use} tag)) 102 | (throw (ex-info (str "Unexpected " tag " before ns definition in " file) {:form form})) 103 | 104 | (#{'require 'use} tag) 105 | (let [requires' (parse-require-form (expand-quotes form)) 106 | requires' (disj requires' ns)] 107 | (recur ns (update-in nses [ns :requires] util/intos requires'))) 108 | 109 | (or 110 | (= 'defonce tag) 111 | (:clj-reload/keep (meta form)) 112 | (and 113 | (list? form) 114 | (:clj-reload/keep (meta (second form))))) 115 | (let [[_ name] form] 116 | (recur ns (assoc-in nses [ns :keep name] {:tag tag 117 | :form form}))) 118 | 119 | :else 120 | (recur ns nses)))))) 121 | 122 | (defn dependees 123 | "Inverts the requies graph. Returns {ns -> #{downstream-ns ...}}" 124 | [namespaces] 125 | (let [*m (volatile! (transient {}))] 126 | (doseq [[from {tos :requires}] namespaces] 127 | (vswap! *m util/update! from #(or % #{})) 128 | (doseq [to tos 129 | :when (namespaces to)] 130 | (vswap! *m util/update! to util/conjs from))) 131 | (persistent! @*m))) 132 | 133 | (defn transitive-closure 134 | "Starts from starts, expands using dependees {ns -> #{downsteram-ns ...}}, 135 | returns #{ns ...}" 136 | [deps starts] 137 | (loop [queue starts 138 | acc (transient #{})] 139 | (let [[start & queue'] queue] 140 | (cond 141 | (empty? queue) 142 | (persistent! acc) 143 | 144 | (contains? acc start) 145 | (recur queue' acc) 146 | 147 | :else 148 | (recur (into queue (deps start)) (conj! acc start)))))) 149 | 150 | (declare topo-sort) 151 | 152 | (defn report-cycle [deps all-deps] 153 | (let [circular (filterv 154 | (fn [node] 155 | (try 156 | (topo-sort (dissoc deps node) (fn [_ _] (throw (ex-info "Part of cycle" {})))) 157 | true 158 | (catch Exception _ 159 | false))) 160 | (keys deps))] 161 | (throw (ex-info (str "Cycle detected: " (str/join ", " (sort circular))) {:nodes circular :deps all-deps})))) 162 | 163 | (defn topo-sort 164 | ([deps] 165 | (topo-sort deps report-cycle)) 166 | ([all-deps on-cycle] 167 | (loop [res (transient []) 168 | deps all-deps] 169 | (if (empty? deps) 170 | (persistent! res) 171 | (let [ends (reduce into #{} (vals deps)) 172 | roots (->> (keys deps) (remove ends) (sort))] 173 | (if (not (empty? roots)) 174 | (recur (reduce conj! res roots) (reduce dissoc deps roots)) 175 | (on-cycle deps all-deps))))))) 176 | -------------------------------------------------------------------------------- /src/clj_reload/util.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.util 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.string :as str]) 5 | (:import 6 | [clojure.lang LineNumberingPushbackReader] 7 | [java.io File StringReader] 8 | [java.net URLClassLoader])) 9 | 10 | (def ^:dynamic *log-fn* 11 | println) 12 | 13 | (defn log [& args] 14 | (when *log-fn* 15 | (apply *log-fn* args))) 16 | 17 | (def reader-opts 18 | {:read-cond :allow 19 | :features #{:clj} 20 | :eof ::eof}) 21 | 22 | (def dummy-resolver 23 | (reify clojure.lang.LispReader$Resolver 24 | (currentNS [_] 25 | 'user) 26 | (resolveClass [_ sym] 27 | sym) 28 | (resolveAlias [_ sym] 29 | (symbol (str ":" sym))) 30 | (resolveVar [_ sym] 31 | sym))) 32 | 33 | (defn read-form [reader] 34 | (binding [*read-eval* false 35 | *suppress-read* true 36 | *reader-resolver* dummy-resolver] 37 | (read reader-opts reader))) 38 | 39 | (defn throwable? [o] 40 | (instance? Throwable o)) 41 | 42 | (defn update! [m k f & args] 43 | (assoc! m k (apply f (m k) args))) 44 | 45 | (def conjs 46 | (fnil conj #{})) 47 | 48 | (def intos 49 | (fnil into #{})) 50 | 51 | (defn assoc-some [m & kvs] 52 | (reduce 53 | (fn [m [k v]] 54 | (cond-> m 55 | (some? v) (assoc k v))) 56 | m 57 | (partition 2 kvs))) 58 | 59 | (defn some-map [& kvs] 60 | (apply assoc-some nil kvs)) 61 | 62 | (defn some-set [& vals] 63 | (not-empty 64 | (set (filter some? vals)))) 65 | 66 | (defn map-vals [m f] 67 | (when (some? m) 68 | (persistent! 69 | (reduce-kv 70 | #(assoc! %1 %2 (f %3)) 71 | (transient (empty m)) 72 | m)))) 73 | 74 | (defn deep-merge [& ms] 75 | (apply merge-with merge ms)) 76 | 77 | (defmacro for-map [& body] 78 | `(into {} 79 | (for ~@body))) 80 | 81 | (defmacro for-set [& body] 82 | `(into #{} 83 | (for ~@body))) 84 | 85 | (defn doeach [f xs] 86 | (doseq [x xs] 87 | (f x))) 88 | 89 | (defn now [] 90 | (System/currentTimeMillis)) 91 | 92 | (defn last-modified [^File f] 93 | (some-> f .lastModified)) 94 | 95 | (defn set-last-modified [^File f t] 96 | (some-> f (.setLastModified t))) 97 | 98 | (defn file? [^File f] 99 | (some-> f .isFile)) 100 | 101 | (defn directory? [^File f] 102 | (some-> f .isDirectory)) 103 | 104 | (defn file-name [^File f] 105 | (some-> f .getName)) 106 | 107 | (defn file-path [^File f] 108 | (some-> f .getPath)) 109 | 110 | (defn file-delete [^File f] 111 | (some-> f .delete)) 112 | 113 | (defn file-reader ^LineNumberingPushbackReader [f] 114 | (LineNumberingPushbackReader. 115 | (io/reader (io/file f)))) 116 | 117 | (defn string-reader ^LineNumberingPushbackReader [^String s] 118 | (LineNumberingPushbackReader. 119 | (StringReader. s))) 120 | 121 | (defn ns-load-file [content ns ^File file] 122 | (let [[_ ext] (re-matches #".*\.([^.]+)" (.getName file)) 123 | path (-> ns str (str/replace #"\-" "_") (str/replace #"\." "/") (str "." ext))] 124 | (Compiler/load (StringReader. content) path (.getName file)))) 125 | 126 | (defn loader-classpath [] 127 | (->> (clojure.lang.RT/baseLoader) 128 | (iterate #(.getParent ^ClassLoader %)) 129 | (take-while identity) 130 | (filter #(instance? URLClassLoader %)) 131 | (mapcat #(.getURLs ^URLClassLoader %)) 132 | (map io/as-file) 133 | (filter directory?))) 134 | 135 | (defn system-classpath [] 136 | (-> (System/getProperty "java.class.path") 137 | (str/split (re-pattern (System/getProperty "path.separator"))) 138 | (->> (map io/as-file) 139 | (filter directory?)))) 140 | 141 | (defn classpath-dirs [] 142 | (->> (or 143 | (not-empty (loader-classpath)) 144 | (not-empty (system-classpath))) 145 | (distinct) 146 | (mapv file-path))) 147 | 148 | (comment 149 | (classpath-dirs) 150 | (system-classpath)) -------------------------------------------------------------------------------- /test/clj_reload/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.core-test 2 | (:require 3 | [clj-reload.core :as reload] 4 | [clj-reload.parse :as parse] 5 | [clj-reload.test-util :as tu] 6 | [clj-reload.util :as util] 7 | [clojure.java.io :as io] 8 | [clojure.test :refer [is deftest use-fixtures]])) 9 | 10 | (defn reset [] 11 | (tu/reset '[two-nses-second two-nses split o n no-unload m l i j k f a g h d c e double b])) 12 | 13 | (defn wrap-test [f] 14 | (binding [tu/*dir* "fixtures/core_test"] 15 | (reset) 16 | (f))) 17 | 18 | (use-fixtures :each wrap-test) 19 | 20 | (defn modify [& syms] 21 | (let [[opts syms] (if (map? (first syms)) 22 | [(first syms) (next syms)] 23 | [nil syms])] 24 | (reset) 25 | (util/doeach require (:require opts)) 26 | (tu/init opts) 27 | (util/doeach tu/touch syms) 28 | (tu/reload opts) 29 | (tu/trace))) 30 | 31 | ; Fixture namespaces dependency plan 32 | ; Top ones require bottom ones 33 | ; 34 | ; a f i l m 35 | ; ╱ │ ╲ ╱ ╲ │ │ 36 | ; b c d h g j n 37 | ; ╲ │ ╱ │ │ 38 | ; e k o 39 | 40 | (deftest find-namespaces-test 41 | (tu/init) 42 | (is (= '#{a b c d double e err-runtime f g h i j k l m n no-reload no-unload o split two-nses two-nses-second} (reload/find-namespaces))) 43 | (is (= '#{a b c d e f g h i j k l m n o} (reload/find-namespaces #"\w"))) 44 | (is (= '#{a b c} (reload/find-namespaces #"[abc]")))) 45 | 46 | (deftest reload-test 47 | (is (thrown-with-msg? IllegalStateException #"clj-reload not initialized. Call `init` first" (tu/reload))) 48 | (let [opts {:require '[b e c d h g a f k j i l]}] 49 | (is (= '["Unloading" a "Loading" a] (modify opts 'a))) 50 | (is (= '["Unloading" a b "Loading" b a] (modify opts 'b))) 51 | (is (= '["Unloading" a c "Loading" c a] (modify opts 'c))) 52 | (is (= '["Unloading" f a d "Loading" d a f] (modify opts 'd))) 53 | (is (= '["Unloading" f a h d c e "Loading" e c d h a f] (modify opts 'e))) 54 | (is (= '["Unloading" f "Loading" f] (modify opts 'f))) 55 | (is (= '["Unloading" f g "Loading" g f] (modify opts 'g))) 56 | (is (= '["Unloading" i "Loading" i] (modify opts 'i))) 57 | (is (= '["Unloading" i j "Loading" j i] (modify opts 'j))) 58 | (is (= '["Unloading" i j k "Loading" k j i] (modify opts 'k))) 59 | (is (= '["Unloading" l "Loading" l] (modify opts 'l))) 60 | (is (= '[] (modify opts))) 61 | (is (= '["Unloading" a c b "Loading" b c a] (modify opts 'a 'b 'c))) 62 | (is (= '["Unloading" i f a j h d c l k e "Loading" e k l c d h j a f i] (modify opts 'e 'k 'l))) 63 | (is (= '["Unloading" i f a j h d c l k g e b "Loading" b e g k l c d h j a f i] (modify opts 'a 'b 'c 'd 'e 'f 'g 'h 'i 'j 'k 'l))))) 64 | 65 | (deftest parse-cache-test 66 | (let [*parsed (atom #{}) 67 | read-file parse/read-file 68 | opts {:require '[b e c d h g a f k j i l]}] 69 | (reset) 70 | (util/doeach require (:require opts)) 71 | (tu/init opts) 72 | (util/doeach tu/touch '[e]) 73 | (with-redefs [parse/read-file (fn 74 | ([file] 75 | (swap! *parsed conj file) 76 | (read-file file)) 77 | ([rdr file] 78 | (swap! *parsed conj file) 79 | (read-file rdr file)))] 80 | (tu/reload opts) 81 | (is (= '["Unloading" f a h d c e "Loading" e c d h a f] (tu/trace))) 82 | (is (= #{(io/file "fixtures/core_test/e.clj")} @*parsed))))) 83 | 84 | (deftest return-value-ok-test 85 | (tu/init 'a 'f 'h) 86 | (is (= {:unloaded '[] 87 | :loaded '[]} (tu/reload))) 88 | (tu/touch 'e) 89 | (is (= {:unloaded '[f a h d c e] 90 | :loaded '[e c d h a f]} (tu/reload)))) 91 | 92 | (deftest return-value-fail-test 93 | (tu/init 'a 'f 'h) 94 | (tu/with-changed 'c "(ns c (:require e)) (/ 1 0)" 95 | (tu/touch 'e) 96 | (try 97 | (tu/reload) 98 | (is (= "Should throw" "Didn't throw")) 99 | (catch Exception e 100 | (is (= {:unloaded '[f a h d c e] 101 | :loaded '[e] 102 | :failed 'c} (ex-data e)))))) 103 | (is (= {:unloaded '[c] 104 | :loaded '[c d h a f]} (tu/reload)))) 105 | 106 | (deftest return-value-fail-safe-test 107 | (tu/init 'a 'f 'h) 108 | (tu/with-changed 'c "(ns c (:require e)) (/ 1 0)" 109 | (tu/touch 'e) 110 | (let [res (tu/reload {:throw false})] 111 | (is (= {:unloaded '[f a h d c e] 112 | :loaded '[e] 113 | :failed 'c} (dissoc res :exception))))) 114 | (is (= {:unloaded '[c] 115 | :loaded '[c d h a f]} (tu/reload)))) 116 | 117 | (deftest reload-active-test 118 | (is (= '["Unloading" a d c e "Loading" e c d a] (modify {:require '[a]} 'e))) 119 | (is (= '["Unloading" a d c e "Loading" e c d a] (modify {:require '[a]} 'e 'h 'g 'f 'k)))) 120 | 121 | (deftest unload-test 122 | (is (thrown-with-msg? IllegalStateException #"clj-reload not initialized. Call `init` first" (tu/unload))) 123 | (tu/init 'a 'f 'h) 124 | (tu/touch 'e) 125 | (tu/unload) 126 | (is (= '["Unloading" f a h d c e] (tu/trace))) 127 | (tu/unload) 128 | (is (= '[] (tu/trace))) 129 | (tu/reload) 130 | (is (= '["Loading" e c d h a f] (tu/trace)))) 131 | 132 | (deftest reload-split-test 133 | (tu/init 'split) 134 | (is (= 1 @(resolve 'split/split-part))) 135 | (tu/with-changed 'split-part "(in-ns 'split) (def split-part 2)" 136 | (tu/reload) 137 | (is (= '["Unloading" split "Loading" split] (tu/trace))) 138 | (is (= 2 @(resolve 'split/split-part))))) 139 | 140 | (deftest reload-double-test 141 | (tu/init 'double) 142 | (is (= :a @(resolve 'double/a))) 143 | (tu/touch 'double) 144 | (tu/reload) 145 | (is (= '["Unloading" double "Loading" double double] (tu/trace))) 146 | (is (= :a @(resolve 'double/a))) 147 | (is (= :b @(resolve 'double/b))) 148 | (tu/with-changed 'double "(ns double) (def a :a2)" 149 | (tu/reload) 150 | (is (= '["Unloading" double "Loading" double double] (tu/trace))) 151 | (is (= :a2 @(resolve 'double/a))) 152 | (is (= :b @(resolve 'double/b)))) 153 | (tu/with-changed 'double-b "(ns double) (def b :b2)" 154 | (tu/reload) 155 | (is (= '["Unloading" double "Loading" double double] (tu/trace))) 156 | (is (= :a @(resolve 'double/a))) 157 | (is (= :b2 @(resolve 'double/b)))) 158 | (tu/reload) 159 | (is (= '["Unloading" double "Loading" double double] (tu/trace))) 160 | (is (= :a @(resolve 'double/a))) 161 | (is (= :b @(resolve 'double/b)))) 162 | 163 | (deftest exclude-test 164 | (let [opts {:require '[b e c d h g a f k j i l]}] 165 | (is (= '[] (modify (assoc opts :no-reload ['k]) 'k))) 166 | (is (= '["Unloading" f a h d e "Loading" e d h a f] (modify (assoc opts :no-reload ['c]) 'e))) 167 | (is (= '["Unloading" f a h d e "Loading" e c d h a f] (modify (assoc opts :no-unload ['c]) 'e))))) 168 | 169 | (deftest reload-loaded-test 170 | (is (= '["Unloading" a d c e b "Loading" b e c d a] (modify {:require '[a] :only :loaded}))) 171 | (is (= '["Unloading" f a d c g e b "Loading" b e g c d a f] (modify {:require '[a f] :only :loaded}))) 172 | (is (= '["Unloading" f a h d c g e b "Loading" b e g c d h a f] (modify {:require '[a f h] :only :loaded})))) 173 | 174 | (deftest reload-regexp-test 175 | (is (= '["Loading" i] (modify {:require '[a f] :only #"(a|i)"}))) 176 | (is (= '["Unloading" a "Loading" a i] (modify {:require '[a f] :only #"(a|i)"} 'a))) 177 | (is (= '["Loading" i] (modify {:require '[a f] :only #"(a|i)"} 'i))) 178 | (is (= '["Unloading" a "Loading" a i] (modify {:require '[a f] :only #"(a|i)"} 'a 'i))) 179 | (is (= '["Unloading" f a d c e "Loading" e c d a f i] (modify {:require '[a f] :only #"(a|i)"} 'e))) 180 | (is (= '["Loading" i] (modify {:require '[a f] :only #"(a|i)"} 'k))) 181 | (is (= '["Unloading" k "Loading" k i] (modify {:require '[a f k] :only #"(a|i)"} 'k)))) 182 | 183 | (deftest reload-all-test 184 | (tu/with-deleted 'err-runtime 185 | (is (= '["Loading" b double double e g k l no-unload o split two-nses two-nses-second c d h j n a f i m] 186 | (modify {:require '[] :only :all}))))) 187 | 188 | (deftest reload-exception-test 189 | (tu/init 'a) 190 | (tu/with-changed 'c "(ns c (:require e)) (/ 1 0)" 191 | (tu/touch 'e) 192 | (is (thrown? Exception (tu/reload))) 193 | (is (= '["Unloading" a d c e "Loading" e c " failed to load" c] (tu/trace))) 194 | (is (thrown? Exception (tu/reload))) 195 | (is (= '["Unloading" c "Loading" c " failed to load" c] (tu/trace)))) 196 | (tu/reload) 197 | (is (= '["Unloading" c "Loading" c d a] (tu/trace)))) 198 | 199 | (deftest reload-unknown-dep-test 200 | (tu/init 'a) 201 | (tu/with-changed 'c "(ns c (:require e z))" 202 | (tu/touch 'e) 203 | (is (thrown? Exception (tu/reload))) 204 | (is (= '["Unloading" a d c e "Loading" e c " failed to load" c] (tu/trace)))) 205 | (tu/reload) 206 | (is (= '["Unloading" c "Loading" c d a] (tu/trace)))) 207 | 208 | (deftest reload-ill-formed-test 209 | (tu/init 'a) 210 | (tu/with-changed 'c "(ns c (:require e" 211 | (tu/touch 'e) 212 | (is (thrown? Exception (tu/reload))) 213 | (is (= '["Failed to read" "fixtures/core_test/c.clj"] (tu/trace)))) 214 | (tu/reload) 215 | (is (= '["Unloading" a d c e "Loading" e c d a] (tu/trace)))) 216 | 217 | (deftest reload-changed-test 218 | (tu/init 'i) 219 | (tu/with-changed 'i "(ns i)" 220 | (tu/with-changed 'j "(ns j (:require i))" 221 | (tu/with-changed 'k "(ns k (:require j))" 222 | (tu/reload) 223 | (is (= '["Unloading" i j k "Loading" i j k] (tu/trace)))))) 224 | (tu/reload) 225 | (is (= '["Unloading" k j i "Loading" k j i] (tu/trace)))) 226 | 227 | (deftest reload-deleted-test 228 | (tu/init 'l) 229 | (tu/with-deleted 'l 230 | (tu/reload) 231 | (is (= '["Unloading" l] (tu/trace))))) 232 | 233 | (deftest reload-deleted-2-test 234 | (tu/init 'i) 235 | (tu/with-changed 'j "(ns j)" 236 | (tu/with-deleted 'k 237 | (tu/reload) 238 | (is (= '["Unloading" i j k "Loading" j i] (tu/trace))))) 239 | (tu/reload) 240 | (is (= '["Unloading" i j "Loading" j i] (tu/trace)))) 241 | 242 | (deftest reload-rename-ns 243 | (tu/init 'i) 244 | (tu/with-changed 'i "(ns z)" 245 | (tu/touch 'k) 246 | (tu/reload) 247 | (is (= '["Unloading" i j k "Loading" k j] (tu/trace)))) 248 | (tu/reload) 249 | (is (= '[] (tu/trace)))) 250 | 251 | (deftest reload-remove-ns 252 | (tu/init 'i) 253 | (tu/with-changed 'i "" 254 | (tu/touch 'k) 255 | (tu/reload) 256 | (is (= '["Unloading" i j k "Loading" k j] (tu/trace)))) 257 | (tu/reload) 258 | (is (= '[] (tu/trace)))) 259 | 260 | (deftest cycle-self-test 261 | (tu/init 'l) 262 | (tu/with-changed 'l "(ns l (:require l))" 263 | (tu/reload) 264 | (is (= '["Unloading" l "Loading" l] (tu/trace)))) 265 | (tu/reload) 266 | (is (= '["Unloading" l "Loading" l] (tu/trace)))) 267 | 268 | (deftest cycle-one-hop-test 269 | (tu/init 'i) 270 | (tu/with-changed 'j "(ns j (:require i))" 271 | (is (thrown-with-msg? Exception #"Cycle detected: i, j" (tu/reload))) 272 | (is (= '[] (tu/trace)))) 273 | (tu/reload) 274 | (is (= '["Unloading" i j "Loading" j i] (tu/trace)))) 275 | 276 | (deftest cycle-two-hops-test 277 | (tu/init 'i) 278 | (tu/with-changed 'k "(ns k (:require i))" 279 | (is (thrown-with-msg? Exception #"Cycle detected: i, j, k" (tu/reload))) 280 | (is (= '[] (tu/trace)))) 281 | (tu/reload) 282 | (is (= '["Unloading" i j k "Loading" k j i] (tu/trace)))) 283 | 284 | (deftest cycle-extra-nodes-test 285 | (tu/init 'a 'f 'h) 286 | (tu/with-changed 'e "(ns e (:require h))" 287 | (is (thrown-with-msg? Exception #"Cycle detected: e, h" (tu/reload))) 288 | (is (= '[] (tu/trace)))) 289 | (tu/reload) 290 | (is (= '["Unloading" f a h d c e "Loading" e c d h a f] (tu/trace)))) 291 | 292 | (deftest hooks-test 293 | (is (= '["Unloading" m n "Loading" n m] (modify {:require '[o n m]} 'n))) 294 | (is (= [:unload-m :unload-n :reload-n :reload-m] @@(resolve 'o/*atom)))) 295 | 296 | (deftest hooks-test-2 297 | (is (= '["Unloading" m "Loading" m] (modify {:require '[o n m]} 'm))) 298 | (is (= [:unload-m :reload-m] @@(resolve 'o/*atom)))) 299 | 300 | (deftest unload-hook-fail-test 301 | (tu/with-changed 'm "(ns m 302 | (:require n o)) 303 | 304 | (defn before-ns-unload [] 305 | (/ 1 0))" 306 | (tu/init 'm) 307 | (tu/touch 'm) 308 | (tu/reload) 309 | (is (= '["Unloading" m " exception during unload hook" "java.lang.ArithmeticException" "Loading" m] (tu/trace)))) 310 | (tu/reload) 311 | (is (= '["Unloading" m " exception during unload hook" "java.lang.ArithmeticException" "Loading" m] (tu/trace)))) 312 | 313 | (deftest reload-hook-fail-test 314 | (tu/init 'm) 315 | (tu/with-changed 'n "(ns n 316 | (:require o)) 317 | 318 | (defn after-ns-reload [] 319 | (/ 1 0))" 320 | (tu/touch 'o) 321 | (is (thrown? Exception (tu/reload))) 322 | (is (= '["Unloading" m n o "Loading" o n " failed to load" n] (tu/trace)))) 323 | (tu/reload) 324 | (is (= '["Unloading" n "Loading" n m] (tu/trace)))) 325 | 326 | (deftest no-unload-meta-test 327 | (tu/init 'no-unload) 328 | (let [rand1 @(resolve 'no-unload/rand1) 329 | rand2 @(resolve 'no-unload/rand2)] 330 | (tu/with-changed 'no-unload "(ns ^:clj-reload/no-unload no-unload) 331 | 332 | (def rand1 333 | (rand-int Integer/MAX_VALUE))" 334 | (tu/reload) 335 | (let [rand1' @(resolve 'no-unload/rand1) 336 | rand2' @(resolve 'no-unload/rand2)] 337 | (is (not= rand1' rand1)) 338 | (is (= rand2' rand2)))))) 339 | 340 | (deftest no-reload-meta-test 341 | (tu/init 'no-reload) 342 | (let [rand1 @(resolve 'no-reload/rand1) 343 | _ (tu/touch 'no-reload) 344 | _ (tu/reload) 345 | rand1' @(resolve 'no-reload/rand1)] 346 | (is (= rand1' rand1)))) 347 | -------------------------------------------------------------------------------- /test/clj_reload/keep_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.keep-test 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.test :refer [is are deftest testing use-fixtures]] 5 | [clj-reload.core :as reload] 6 | [clj-reload.keep :as keep] 7 | [clj-reload.test-util :as tu])) 8 | 9 | (defn wrap-test [f] 10 | (binding [tu/*dir* "fixtures/keep_test"] 11 | (tu/reset 12 | '[clj-reload.dependency 13 | clj-reload.keep-custom 14 | clj-reload.keep-defprotocol 15 | clj-reload.keep-defrecord 16 | clj-reload.keep-deftype 17 | clj-reload.keep-vars 18 | clj-reload.keep-downstream 19 | clj-reload.keep-upstream]) 20 | (f))) 21 | 22 | (use-fixtures :each wrap-test) 23 | 24 | (deftest patch-file-test 25 | (is (= "before (def *atom 888) after" 26 | (keep/patch-file 27 | "before (defonce *atom 777) after" 28 | {'(defonce *atom) "(def *atom 888)"}))) 29 | 30 | (is (= "before (def *atom 1000000) after" 31 | (keep/patch-file 32 | "before (def *atom 1) after" 33 | {'(def *atom) "(def *atom 1000000)"}))) 34 | 35 | (is (= "before (def *atom 888) 36 | after" 37 | (keep/patch-file 38 | "before (defonce *atom 39 | 777) after" 40 | {'(defonce *atom) "(def *atom 888)"}))) 41 | 42 | (is (= "before (def *atom 888) (reset! *atom nil) after" 43 | (keep/patch-file 44 | "before (def *atom 777) (reset! *atom nil) after" 45 | {'(def *atom) "(def *atom 888)"}))) 46 | 47 | (is (= "(ns keep) 48 | 49 | asdas 50 | 51 | 8 10 (def *atom 777) 52 | 53 | 54 | (def just-var 888) 55 | " 56 | (keep/patch-file 57 | "(ns keep) 58 | 59 | asdas 60 | 61 | 8 10 (defonce *atom 62 | (atom nil)) 63 | 64 | (defonce just-var 65 | (Object.))" 66 | {'(defonce *atom) "(def *atom 777)" 67 | '(defonce just-var) "(def just-var 888)"})))) 68 | 69 | (defn meta= [a b] 70 | (= (dissoc (meta a) :ns) (dissoc (meta b) :ns))) 71 | 72 | (deftest keep-vars-test 73 | (tu/init 'clj-reload.keep-vars) 74 | (let [ns (find-ns 'clj-reload.keep-vars) 75 | normal @(ns-resolve ns 'normal) 76 | atom (reset! @(ns-resolve ns '*atom) 100500) 77 | just-var @(ns-resolve ns 'just-var) 78 | just-var-2 @(ns-resolve ns 'just-var-2) 79 | private-var @(ns-resolve ns 'private-var) 80 | dependent @(ns-resolve ns 'dependent) 81 | meta-var (ns-resolve ns 'meta-var) 82 | public-fn (ns-resolve ns 'public-fn) 83 | private-fn (ns-resolve ns 'private-fn) 84 | normal-2 @(ns-resolve ns 'normal-2) 85 | 86 | _ (tu/touch 'clj-reload.keep-vars) 87 | _ (tu/reload) 88 | ns' (find-ns 'clj-reload.keep-vars)] 89 | 90 | (is (not= normal @(ns-resolve ns' 'normal))) 91 | (is (= atom @@(ns-resolve ns' '*atom))) 92 | (is (= just-var @(ns-resolve ns' 'just-var))) 93 | (is (= just-var-2 @(ns-resolve ns' 'just-var-2))) 94 | (is (= private-var @(ns-resolve ns' 'private-var))) 95 | (is (= (first dependent) (first @(ns-resolve ns' 'dependent)))) 96 | (is (not= (second dependent) (second @(ns-resolve ns' 'dependent)))) 97 | 98 | (is (= @meta-var @(ns-resolve ns' 'meta-var))) 99 | (is (meta= meta-var (ns-resolve ns' 'meta-var))) 100 | 101 | (is (= @public-fn @(ns-resolve ns' 'public-fn))) 102 | (is (meta= public-fn (ns-resolve ns' 'public-fn))) 103 | 104 | (is (= @private-fn @(ns-resolve ns' 'private-fn))) 105 | (is (meta= private-fn (ns-resolve ns' 'private-fn))) 106 | 107 | (is (not= normal-2 @(ns-resolve ns' 'normal-2))))) 108 | 109 | (deftest issue-22-keep-double-reload 110 | (tu/init 'clj-reload.keep-vars) 111 | (let [ns (find-ns 'clj-reload.keep-vars) 112 | atom (reset! @(ns-resolve ns '*atom) 1) 113 | 114 | _ (tu/touch 'clj-reload.dependency) 115 | _ (tu/reload) 116 | atom' @@(ns-resolve (find-ns 'clj-reload.keep-vars) '*atom) 117 | _ (is (= atom atom')) 118 | 119 | _ (tu/touch 'clj-reload.dependency) 120 | _ (tu/reload) 121 | atom'' @@(ns-resolve (find-ns 'clj-reload.keep-vars) '*atom) 122 | _ (is (= atom atom''))])) 123 | 124 | (deftest keep-unsupported-test 125 | (tu/init 'clj-reload.keep-unsupported) 126 | (let [ns (find-ns 'clj-reload.keep-unsupported) 127 | v @(ns-resolve ns 'v)] 128 | (tu/touch 'clj-reload.keep-unsupported) 129 | (is (thrown? Exception (tu/reload))))) 130 | 131 | (deftest keep-type-test 132 | (tu/init 'clj-reload.keep-deftype) 133 | (let [ns (find-ns 'clj-reload.keep-deftype) 134 | normal-new @(ns-resolve ns 'type-normal-new) 135 | normal-factory @(ns-resolve ns 'type-normal-factory) 136 | keep-new @(ns-resolve ns 'type-keep-new) 137 | keep-factory @(ns-resolve ns 'type-keep-factory) 138 | _ (tu/touch 'clj-reload.keep-deftype) 139 | _ (tu/reload) 140 | ns' (find-ns 'clj-reload.keep-deftype)] 141 | (is (not= normal-new @(ns-resolve ns' 'type-normal-new))) 142 | (is (not (identical? (class normal-new) (class @(ns-resolve ns' 'type-normal-new))))) 143 | 144 | (is (not= normal-factory @(ns-resolve ns' 'type-normal-factory))) 145 | (is (not (identical? (class normal-factory) (class @(ns-resolve ns' 'type-normal-factory))))) 146 | 147 | (is (not (identical? keep-new @(ns-resolve ns' 'type-keep-new)))) 148 | (is (= keep-new @(ns-resolve ns' 'type-keep-new))) 149 | (is (identical? (class keep-new) (class @(ns-resolve ns' 'type-keep-new)))) 150 | 151 | (is (not (identical? keep-factory @(ns-resolve ns' 'type-keep-factory)))) 152 | (is (= keep-factory @(ns-resolve ns' 'type-keep-factory))) 153 | (is (identical? (class keep-factory) (class @(ns-resolve ns' 'type-keep-factory)))))) 154 | 155 | (deftest keep-record-test 156 | (tu/init 'clj-reload.keep-defrecord) 157 | (let [ns (find-ns 'clj-reload.keep-defrecord) 158 | normal-new @(ns-resolve ns 'record-normal-new) 159 | normal-factory @(ns-resolve ns 'record-normal-factory) 160 | normal-map-factory @(ns-resolve ns 'record-normal-map-factory) 161 | keep-new @(ns-resolve ns 'record-keep-new) 162 | keep-factory @(ns-resolve ns 'record-keep-factory) 163 | keep-map-factory @(ns-resolve ns 'record-keep-map-factory) 164 | _ (tu/touch 'clj-reload.keep-defrecord) 165 | _ (tu/reload) 166 | ns' (find-ns 'clj-reload.keep-defrecord)] 167 | (is (not= normal-new @(ns-resolve ns' 'record-normal-new))) 168 | (is (not (identical? (class normal-new) (class @(ns-resolve ns' 'record-normal-new))))) 169 | 170 | (is (not= normal-factory @(ns-resolve ns' 'record-normal-factory))) 171 | (is (not (identical? (class normal-factory) (class @(ns-resolve ns' 'record-normal-factory))))) 172 | 173 | (is (not= normal-map-factory @(ns-resolve ns' 'record-normal-map-factory))) 174 | (is (not (identical? (class normal-map-factory) (class @(ns-resolve ns' 'record-normal-map-factory))))) 175 | 176 | (is (not (identical? keep-new @(ns-resolve ns' 'record-keep-new)))) 177 | (is (= keep-new @(ns-resolve ns' 'record-keep-new))) 178 | (is (identical? (class keep-new) (class @(ns-resolve ns' 'record-keep-new)))) 179 | 180 | (is (not (identical? keep-factory @(ns-resolve ns' 'record-keep-factory)))) 181 | (is (= keep-factory @(ns-resolve ns' 'record-keep-factory))) 182 | (is (identical? (class keep-factory) (class @(ns-resolve ns' 'record-keep-factory)))) 183 | 184 | (is (not (identical? keep-map-factory @(ns-resolve ns' 'record-keep-map-factory)))) 185 | (is (= keep-map-factory @(ns-resolve ns' 'record-keep-map-factory))) 186 | (is (identical? (class keep-map-factory) (class @(ns-resolve ns' 'record-keep-map-factory)))))) 187 | 188 | (defmethod reload/keep-methods 'deftype+ [_] 189 | (reload/keep-methods 'deftype)) 190 | 191 | (deftest keep-custom-def-test 192 | (tu/init 'clj-reload.keep-custom) 193 | (let [ns (find-ns 'clj-reload.keep-custom) 194 | ctor @(ns-resolve ns '->CustomTypeKeep) 195 | value @(ns-resolve ns 'custom-type-keep) 196 | _ (tu/touch 'clj-reload.keep-custom) 197 | _ (tu/reload) 198 | ns' (find-ns 'clj-reload.keep-custom)] 199 | (is (identical? ctor @(ns-resolve ns' '->CustomTypeKeep))) 200 | (is (identical? (class value) (class @(ns-resolve ns' 'custom-type-keep)))))) 201 | 202 | (deftest keep-protocol-test 203 | (tu/init 'clj-reload.keep-defprotocol) 204 | (let [ns (find-ns 'clj-reload.keep-defprotocol) 205 | proto @(ns-resolve ns 'IProto) 206 | method @(ns-resolve ns '-method) 207 | rec-inline @(ns-resolve ns 'rec-inline) 208 | rec-extend-proto @(ns-resolve ns 'rec-extend-proto) 209 | rec-extend-type @(ns-resolve ns 'rec-extend-type) 210 | rec-extend @(ns-resolve ns 'rec-extend) 211 | extend-meta @(ns-resolve ns 'extend-meta) 212 | 213 | _ (tu/touch 'clj-reload.keep-defprotocol) 214 | _ (tu/reload) 215 | 216 | ns' (find-ns 'clj-reload.keep-defprotocol) 217 | proto' @(ns-resolve ns' 'IProto) 218 | method' @(ns-resolve ns' '-method) 219 | rec-inline' @(ns-resolve ns' 'rec-inline) 220 | rec-extend-proto' @(ns-resolve ns' 'rec-extend-proto) 221 | rec-extend-type' @(ns-resolve ns' 'rec-extend-type) 222 | rec-extend' @(ns-resolve ns' 'rec-extend) 223 | extend-meta' @(ns-resolve ns' 'extend-meta)] 224 | 225 | ;; make sure reload happened 226 | (is (not (identical? rec-inline rec-inline'))) 227 | (is (not (identical? (class rec-inline) (class rec-inline')))) 228 | 229 | (is (satisfies? proto rec-inline)) 230 | (is (satisfies? proto rec-inline')) 231 | (is (satisfies? proto' rec-inline)) 232 | (is (satisfies? proto' rec-inline')) 233 | (is (= :rec-inline (method rec-inline))) 234 | (is (= :rec-inline (method rec-inline'))) 235 | (is (= :rec-inline (method' rec-inline))) 236 | (is (= :rec-inline (method' rec-inline'))) 237 | 238 | (is (satisfies? proto rec-extend-proto)) 239 | ; (is (satisfies? proto rec-extend-proto')) 240 | (is (satisfies? proto' rec-extend-proto)) 241 | (is (satisfies? proto' rec-extend-proto')) 242 | (is (= :rec-extend-proto (method rec-extend-proto))) 243 | ; (is (= :rec-extend-proto (method rec-extend-proto'))) 244 | (is (= :rec-extend-proto (method' rec-extend-proto))) 245 | (is (= :rec-extend-proto (method' rec-extend-proto'))) 246 | 247 | (is (satisfies? proto rec-extend-type)) 248 | ; (is (satisfies? proto rec-extend-type')) 249 | (is (satisfies? proto' rec-extend-type)) 250 | (is (satisfies? proto' rec-extend-type')) 251 | (is (= :rec-extend-type (method rec-extend-type))) 252 | ; (is (= :rec-extend-type (method rec-extend-type'))) 253 | (is (= :rec-extend-type (method' rec-extend-type))) 254 | (is (= :rec-extend-type (method' rec-extend-type'))) 255 | 256 | (is (satisfies? proto rec-extend)) 257 | ; (is (satisfies? proto rec-extend')) 258 | (is (satisfies? proto' rec-extend)) 259 | (is (satisfies? proto' rec-extend')) 260 | (is (= :rec-extend (method rec-extend))) 261 | ; (is (= :rec-extend (method rec-extend'))) 262 | (is (= :rec-extend (method' rec-extend))) 263 | (is (= :rec-extend (method' rec-extend'))) 264 | 265 | ; (is (satisfies? proto extend-meta)) 266 | ; (is (satisfies? proto extend-meta')) 267 | ; (is (satisfies? proto' extend-meta)) 268 | ; (is (satisfies? proto' extend-meta')) 269 | (is (= :extend-meta (method extend-meta))) 270 | (is (= :extend-meta (method extend-meta'))) 271 | (is (= :extend-meta (method' extend-meta))) 272 | (is (= :extend-meta (method' extend-meta'))))) 273 | 274 | (deftest keep-dependent-test 275 | (tu/init 'clj-reload.keep-downstream) 276 | (let [downstream @(ns-resolve (find-ns 'clj-reload.keep-downstream) 'downstream-var) 277 | upstream @(ns-resolve (find-ns 'clj-reload.keep-upstream) 'upstream-var) 278 | _ (tu/touch 'clj-reload.keep-upstream) 279 | _ (tu/reload) 280 | downstream' @(ns-resolve (find-ns 'clj-reload.keep-downstream) 'downstream-var) 281 | upstream' @(ns-resolve (find-ns 'clj-reload.keep-upstream) 'upstream-var)] 282 | (is (= downstream downstream')) 283 | (is (= upstream upstream')))) 284 | 285 | (deftest keep-dependent-broken-test 286 | (tu/init 'clj-reload.keep-downstream) 287 | (let [downstream @(ns-resolve (find-ns 'clj-reload.keep-downstream) 'downstream-var) 288 | upstream @(ns-resolve (find-ns 'clj-reload.keep-upstream) 'upstream-var)] 289 | (tu/with-changed 'clj-reload.keep-upstream "(ns clj-reload.keep-upstream) 290 | oops!" 291 | (is (thrown? Exception (tu/reload)))) 292 | (tu/reload) 293 | (let [downstream' @(ns-resolve (find-ns 'clj-reload.keep-downstream) 'downstream-var) 294 | upstream' @(ns-resolve (find-ns 'clj-reload.keep-upstream) 'upstream-var)] 295 | (is (= downstream downstream')) 296 | (is (= upstream upstream'))))) 297 | 298 | (deftest keep-changing-test 299 | (tu/init 'clj-reload.keep-upstream) 300 | (let [upstream @(ns-resolve (find-ns 'clj-reload.keep-upstream) 'upstream-var)] 301 | (tu/with-changed 'clj-reload.keep-upstream "(ns clj-reload.keep-upstream)" 302 | ;; remove defonce 303 | (tu/reload)) 304 | ;; add defonce 305 | (tu/reload) 306 | (let [upstream' @(ns-resolve (find-ns 'clj-reload.keep-upstream) 'upstream-var)] 307 | (is (not= upstream upstream'))))) 308 | -------------------------------------------------------------------------------- /test/clj_reload/parse_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.parse-test 2 | (:require 3 | [clj-reload.core :as reload] 4 | [clj-reload.parse :as parse] 5 | [clj-reload.util :as util] 6 | [clojure.java.io :as io] 7 | [clojure.test :refer [is deftest testing use-fixtures]]) 8 | (:import 9 | [java.io PushbackReader StringReader StringWriter])) 10 | 11 | (defn read-str [s] 12 | (parse/read-file (PushbackReader. (StringReader. s)) nil)) 13 | 14 | (deftest read-file-test 15 | (is (= '{x {:requires 16 | #{a.b.c 17 | a.b.d 18 | a.b.e 19 | a.b.f 20 | a.b.g 21 | a.b.h 22 | a.b.i 23 | a.b.j 24 | a.b.k 25 | a.b.l} 26 | :keep {x {:tag defonce 27 | :form (defonce x 1)} 28 | y {:tag defprotocol 29 | :form (defprotocol y 2)}}}} 30 | (read-str "(ns x 31 | (:require 32 | a.b.c 33 | [a.b.d] 34 | [a.b.e :as e] 35 | [a.b f g] 36 | [a.b [h :as h]] 37 | [a.b.x :as-alias x] 38 | [a.b [y :as-alias y]]) 39 | (:require 40 | a.b.i) 41 | (:use 42 | a.b.j)) 43 | ... 44 | (defonce x 1) 45 | ... 46 | (require 'a.b.k) 47 | (require '[a.b.z :as-alias z]) 48 | ... 49 | ^:clj-reload/keep 50 | (defprotocol y 2) 51 | ... 52 | (use 'a.b.l)"))) 53 | 54 | (is (= '{x nil} 55 | (read-str "(ns x)"))) 56 | 57 | (is (= '{x {:meta {:clj-reload/no-reload true}}} 58 | (read-str "(ns ^:clj-reload/no-reload x)"))) 59 | 60 | (is (= '{x nil} 61 | (read-str "(in-ns 'x)")))) 62 | 63 | (deftest self-reference-test 64 | (is (= '{x {:requires #{y z}} 65 | y {:requires #{x z}} 66 | z {:requires #{x y}}} 67 | (read-str "(ns x (:require x y z)) 68 | (ns y (:require x y z)) 69 | (ns z (:require x y z))"))) 70 | (is (= '{x {:requires #{y z}} 71 | y {:requires #{x z}} 72 | z {:requires #{x y}}} 73 | (read-str "(ns x) 74 | (require 'x 'y 'z) 75 | (ns y) 76 | (require 'x 'y 'z) 77 | (ns z) 78 | (require 'x 'y 'z)")))) 79 | 80 | (deftest read-file-errors-test 81 | (let [file "(ns x 82 | (:require 123) 83 | (:require [345]) 84 | (:require [567 :as a]) 85 | (:require [789 a b c]))" 86 | out (StringWriter.) 87 | res (binding [*out* out] 88 | (read-str file))] 89 | (is (= '{x nil} res)) 90 | (is (= "Unexpected :require form: 123 91 | Unexpected :require form: [345] 92 | Unexpected :require form: [567 :as a] 93 | Unexpected :require form: [789 a b c] 94 | " (str out))))) 95 | 96 | (deftest reader-test 97 | (is (= {} (read-str "#?(:clj 1 :cljs 2)"))) 98 | (is (= {} (read-str "{:a 1 #?@(:clj [:b 2] :cljs [:c 3])}"))) 99 | (is (= {} (read-str "#user.Y {:a 1}"))) 100 | (is (= {} (read-str "#x 1"))) 101 | (is (= {} (read-str "::kw"))) 102 | (is (= {} (read-str "::abc/kw"))) 103 | (is (= {} (read-str "java.io.File"))) 104 | (is (= {} (read-str "File"))) 105 | (is (= {} (read-str "{:x 1, ::x 2}"))) 106 | (is (= {} (read-str "{:x/y 1, ::x/y 2}")))) ; issue-21 107 | 108 | (deftest scan-impl-test 109 | (let [{files :files' 110 | nses :namespaces'} (binding [reload/*config* {:dirs ["fixtures"] 111 | :files #".*\.cljc?"} 112 | util/*log-fn* nil] 113 | (@#'reload/scan-impl nil 0))] 114 | (testing "no-ns" 115 | (is (= '#{} 116 | (get-in files [(io/file "fixtures/core_test/no_ns.clj") :namespaces])))) 117 | 118 | (testing "two-nses" 119 | (is (= '#{two-nses two-nses-second} 120 | (get-in files [(io/file "fixtures/core_test/two_nses.clj") :namespaces]))) 121 | 122 | (is (= '#{clojure.string} 123 | (get-in nses ['two-nses :requires]))) 124 | 125 | (is (= '#{clojure.set} 126 | (get-in nses ['two-nses-second :requires])))) 127 | 128 | (testing "split" 129 | (is (= '#{split} 130 | (get-in files [(io/file "fixtures/core_test/split.clj") :namespaces]))) 131 | 132 | (is (= '#{split} 133 | (get-in files [(io/file "fixtures/core_test/split_part.clj") :namespaces]))) 134 | 135 | (is (= '#{clojure.string clojure.set} 136 | (get-in nses ['split :requires]))) 137 | 138 | (is (= #{(io/file "fixtures/core_test/split.clj")} 139 | (get-in nses ['split :ns-files]))) 140 | 141 | (is (= #{(io/file "fixtures/core_test/split_part.clj")} 142 | (get-in nses ['split :in-ns-files])))) 143 | 144 | (testing "double" 145 | (is (= '#{double} 146 | (get-in files [(io/file "fixtures/core_test/double.clj") :namespaces]))) 147 | 148 | (is (= '#{double} 149 | (get-in files [(io/file "fixtures/core_test/double_b.clj") :namespaces]))) 150 | 151 | (is (= '#{clojure.string clojure.set} 152 | (get-in nses ['double :requires]))) 153 | 154 | (is (= #{(io/file "fixtures/core_test/double.clj") (io/file "fixtures/core_test/double_b.clj")} 155 | (get-in nses ['double :ns-files])))) 156 | 157 | (testing "custom-file-types" 158 | (let [{files-custom :files'} (binding [reload/*config* {:dirs ["fixtures"] 159 | :files #".*\.(?:cljc?|repl)"} 160 | util/*log-fn* nil] 161 | (@#'reload/scan-impl nil 0))] 162 | (is (nil? 163 | (get-in files [(io/file "fixtures/core_test/custom_file_type.repl") :namespaces]))) 164 | (is (= '#{custom-file-type} 165 | (get-in files-custom [(io/file "fixtures/core_test/custom_file_type.repl") :namespaces]))))))) 166 | -------------------------------------------------------------------------------- /test/clj_reload/test_util.clj: -------------------------------------------------------------------------------- 1 | (ns clj-reload.test-util 2 | (:require 3 | [clj-reload.core :as reload] 4 | [clj-reload.util :as util] 5 | [clojure.java.io :as io] 6 | [clojure.string :as str])) 7 | 8 | (def *trace 9 | (atom [])) 10 | 11 | (defn trace [] 12 | (let [[trace _] (reset-vals! *trace [])] 13 | trace)) 14 | 15 | (def *time 16 | (atom (util/now))) 17 | 18 | (def ^:dynamic *dir*) 19 | 20 | (defn reset [nses] 21 | (reset! *trace []) 22 | (reset! @#'reload/*state {}) 23 | (alter-var-root #'reload/*config* (constantly {})) 24 | (let [now (util/now)] 25 | (reset! *time now) 26 | (doseq [file (next (file-seq (io/file *dir*))) 27 | :when (> (util/last-modified file) now)] 28 | (util/set-last-modified file now))) 29 | (doseq [ns nses] 30 | (when (@@#'clojure.core/*loaded-libs* ns) 31 | (remove-ns ns) 32 | (dosync 33 | (alter @#'clojure.core/*loaded-libs* disj ns))))) 34 | 35 | (defn sym->file [sym] 36 | (-> sym 37 | name 38 | (str/replace "-" "_") 39 | (str/replace "." "/") 40 | (str ".clj") 41 | (->> 42 | (str *dir* "/") 43 | (io/file)))) 44 | 45 | (defn touch [sym] 46 | (let [now (swap! *time + 1000) 47 | file (sym->file sym)] 48 | (util/set-last-modified file now))) 49 | 50 | (defmacro with-changed [sym content' & body] 51 | `(let [sym# ~sym 52 | file# (sym->file sym#) 53 | content# (slurp file#)] 54 | (try 55 | (spit file# ~content') 56 | (touch sym#) 57 | ~@body 58 | (finally 59 | (spit file# content#) 60 | (touch sym#))))) 61 | 62 | (defmacro with-deleted [sym & body] 63 | `(let [sym# ~sym 64 | file# (sym->file sym#) 65 | content# (slurp file#)] 66 | (try 67 | (util/file-delete file#) 68 | ~@body 69 | (finally 70 | (spit file# content#) 71 | (touch sym#))))) 72 | 73 | (defn log-fn [& args] 74 | (swap! *trace 75 | (fn [track] 76 | (let [[type arg & _] args 77 | last-type (->> track (filter string?) last)] 78 | (cond 79 | (nil? arg) track 80 | (= "fixtures/core_test/err_parse.clj" arg) track 81 | (= " exception during unload hook" type) (conj track type (.getName (class arg))) 82 | (not= type last-type) (conj track type arg) 83 | :else (conj track arg)))))) 84 | 85 | (defn init [& args] 86 | (let [[opts nses] (if (map? (first args)) 87 | [(first args) (next args)] 88 | [nil args])] 89 | (reload/init 90 | (merge 91 | {:dirs [*dir*]} 92 | opts)) 93 | (when-not (empty? nses) 94 | (apply require nses)))) 95 | 96 | (defn unload 97 | ([] 98 | (unload nil)) 99 | ([opts] 100 | (reload/unload 101 | (merge {:log-fn log-fn} opts)))) 102 | 103 | (defn reload 104 | ([] 105 | (reload nil)) 106 | ([opts] 107 | (reload/reload 108 | (merge {:log-fn log-fn} opts)))) 109 | --------------------------------------------------------------------------------