{"id":22901766,"url":"https://github.com/mbuczko/revolt","last_synced_at":"2025-10-24T00:03:55.498Z","repository":{"id":62432353,"uuid":"131534520","full_name":"mbuczko/revolt","owner":"mbuczko","description":"Your trampoline to Clojure dev toolbox","archived":false,"fork":false,"pushed_at":"2023-12-05T22:20:47.000Z","size":180,"stargazers_count":52,"open_issues_count":2,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-28T12:45:41.357Z","etag":null,"topics":["clojure","tools-deps"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mbuczko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-04-29T22:19:43.000Z","updated_at":"2024-05-31T07:53:11.000Z","dependencies_parsed_at":"2022-11-01T21:01:03.244Z","dependency_job_id":null,"html_url":"https://github.com/mbuczko/revolt","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Frevolt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Frevolt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Frevolt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mbuczko%2Frevolt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mbuczko","download_url":"https://codeload.github.com/mbuczko/revolt/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":229636737,"owners_count":18102351,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","tools-deps"],"created_at":"2024-12-14T01:40:49.271Z","updated_at":"2025-10-24T00:03:45.486Z","avatar_url":"https://github.com/mbuczko.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Clojars Project](https://img.shields.io/clojars/v/defunkt/revolt.svg)](https://clojars.org/defunkt/revolt)\n\n# Introduction\n\n_TL;DR: revolt is a plugins/tasks oriented library which makes it easier to integrate beloved dev tools like nrepl, rebel readline\nor clojurescript into application, based on Cognitect's [command line tools](https://clojure.org/guides/deps_and_cli)._\n\n_To see it in action look at [revolt-edge](https://github.com/mbuczko/revolt-edge) example._\n\n\nClojure projects historically leverage the power of two glorious build tools: [leiningen](https://leiningen.org/) and [boot](http://boot-clj.com/). Both battle-tested,\nfeature rich alternatives allow to choose either declarative (leiningen) or fully programmable way to manage with tons of dependencies, _dev_ / _prod_ builds and whole\nbunch of tasks crucial for clojure developer.\n\nThe choice was relatively easy. Up util recent.\n\nOne day, according to their tradition, Cognitect surprisingly announced a new player on the stage - a command line tools for running Clojure programs \n([Deps and CLI Guide](https://clojure.org/guides/deps_and_cli)). It was not only about a new tool, the most significant improvement presented to community was \nentirely new way of working with dependencies. Things like multi-module project with separate dependencies and dependencies stright from git repo became possible\njust right out of the box.\n\nDespite of this awesomeness people started wondering how to join all these toys together. Drop well known lein/boot and go along with Cognitect's way or to use\nnew deps for application-specific dependencies and still leverage boot/lein tools for development only? Or maybe ignore new kid on the block and stick with bullet-proof\ntools we used for ages?\n\nOne of most interesting moves within this area was JUXT's [edge](https://juxt.pro/blog/posts/edge.html) and their attempt to build a simple but complete Clojure project\nbased on the newest and most experimental things found around. Yeah, including Cognitect's new dependencies.\n\n_Revolt_ is inspired by JUXT's edge and, at its core, tries to simplify attaching all these shiny tools to the project by gathering them in form of tasks and plugins.\nAnd yes, it depends on Cognitect's dependencies underneath and makes heavy use of newly introduced aliases by the way.\n\nDoesn't it sound like a [Lisp Curse](http://www.winestockwebdesign.com/Essays/Lisp_Curse.html) again? :)\n\n# What's in the box?\n\nA couple of plugins you may really like:\n\n- [x] [Rebel REPL](https://github.com/bhauman/rebel-readline) to give you best REPL experience\n- [x] [Figwheel](https://github.com/bhauman/lein-figwheel) must-have for web development\n- [x] [nREPL](https://github.com/clojure/tools.nrepl) obviously to let you use Emacs and Cider\n- [x] Filesystem watcher able to watch and react on files changes\n\nand a few built-in tasks:\n\n- [x] scss - transforms scss files into css\n- [x] cljs - a cljs compiler\n- [x] aot - ahead-of-time compilation\n- [x] jar - jar packager\n- [x] test - clojure.test runner based on Metosin's [bat-test](https://github.com/metosin/bat-test)\n- [x] info - project info (name, description, package, version, git branch, sha...)\n- [x] assets - static assets fingerprinting\n- [x] capsule - [capsule](http://www.capsule.io) packaging\n- [x] codox - API documentation with [codox](https://github.com/weavejester/codox)\n\nExternal tasks planned:\n- [x] [migrations](https://github.com/mbuczko/revolt-flyway-task) - [flyway](https://flywaydb.org/) based database migrations\n- [x] [catapulte](https://github.com/mbuczko/revolt-catapulte-task) - jar files installer/deployer\n- [ ] lint - linter based on [eastwood](https://github.com/jonase/eastwood)\n- [ ] analyse - static code analyzer based on [kibit](https://github.com/jonase/kibit)\n- [ ] ancient - looking for outdated dependencies\n\n## Plugins\n\nPlugins are these guys who always cause problems. No matter if that's boot or lein, they just barely fit into architecture with what they do. And they do a lot of\nweird things, eg. nREPL is a socket server waiting for connection, REPL is a command line waiting for input, watcher on the other hand is a never ending loop watching\nfor file changes. Apples and oranges put together into same basket.\n\n_Revolt_ does not try to unify them and pretend they're same tasks as cljs compilation or scss transformation. They are simply a piece of code which starts when asked and\nstops working when JVM shuts down. Nothing more than that. Technically, plugins (as well as tasks) are identified by qualified keyword and configured in a separate file.\nTypical configuration looks like following:\n\n```clojure\n{:revolt.plugin/nrepl    {:port 5600}\n :revolt.plugin/rebel    {:init-ns \"foo.system\"}\n :revolt.plugin/figwheel {:builds [\"main\"]}\n :revolt.plugin/watch    {:excluded-paths [\"src/clj\"]\n                          :on-change {:revolt.task/sass \"glob:assets/styles/*.scss\"}}}\n```\nRight after activation plugins usually stay in a separate thread until deactivation phase hits them in a back which happens on JVM shutdown, triggered for example when plugin \nrunning in a main thread (like `rebel`) gets interrupted.\n\nPlugins love to delegate their job down to someone else, as all those bad guys do. In our cruel world these are _tasks_ who handle most of ungrateful work on behalf of Master Plugins.\nAs an example: `watch` plugin observes changes in a filesytem and calls a `sass` task when *.scss file is altered. Sometimes, task _has_ to be explicitly configured to have plugin\nworking, as it takes place for example in `figwheel` case which needs `cljs` task configured to run.\n\nOk, but how to specify which plugins do we want to activate? This is where `clj` tool from Cognitect comes onto scene, but more on that a bit later...\n\n## Tasks\n\nIf we called plugins as \"bad guys\", tasks are definitely the opposite - kind of little dwarfs who are specialized to do one job and do it well. And similar to plugins, there is a bunch\nof built-in tasks ready to serve you and take care of building and packaging your application. Oh, and they can generate documentation too.\n\nTo understand how tasks work, imagine them as a chain of dwarfs, each of them doing specific job and passing result to the next one:\n\n    clean ⇒ info ⇒ sass ⇒ cljs ⇒ capsule\n\nwhich can expressed as a composition:\n\n    (capsule (cljs (sass (info (clean)))))\n\nor in a bit more clojurey way:\n\n``` clojure\n(def build (comp capsule cljs sass info clean))\n```\n\nThis way calling a `build` composition will clean a target directory, generate project information (name, package, version, git sha...), generate CSSes and finally pack everything into an uberjar \n(a capsule actually). Each of these tasks may generate intermediate result and pass it as a map to the next one in a `context`, eg. `info` task gathers project related information which is at the end\npassed to `capsule` which in turn makes use of these bits to generate a correct package.\n\nTo have even more fun, each task can be pre-configured in a very similar way as plugins are:\n\n``` clojure\n:revolt.task/info  {:name \"foo\"\n                    :package bar.bazz\n                    :version \"0.0.1\"\n                    :description \"My awesome project\"}\n\n:revolt.task/test  {:report :pretty}\n\n:revolt.task/sass  {:source-path \"assets/styles\"\n                    :output-path \"styles\"}\n\n:revolt.task/codox {:source-paths [\"src/clj\"]\n                    :source-uri \"http://github.com/fuser/foo/blob/{version}/{filepath}#L{line}\"\n                    :namespaces [foo.main foo.core]}\n\n:revolt.task/cljs  {:compiler {:optimizations :none\n                               :output-dir \"scripts/out\"\n                               :asset-path \"/scripts/core\"\n                               :preloads [devtools.preload]}\n                    :builds [{:id \"main-ui\"\n                              :source-paths [\"src/cljs\"]\n                              :compiler {:main \"foo.main\"\n                                         :output-to \"scripts/main.js\"}}]}\n\n:revolt.task/assets {:assets-paths [\"assets\"]}\n\n:revolt.task/capsule {:exclude-paths #{\"test\" \"src/cljs\"}\n                      :output-jar \"dist/foo.jar\"\n                      :capsule-type :fat\n                      :main \"foo.main\"\n                      :min-java-version \"1.8.0\"\n                      :jvm-args \"-server\"\n                      :caplets {\"MavenCapsule\" [[\"Repositories\" \"central clojars(https://repo.clojars.org/)\"]\n                                                [\"Allow-Snapshots\" \"true\"]]}}\n```\n\nLet's talk about task arguments now.\n\nHaving tasks configured doesn't mean they are sealed and can't be extended in current REPL session any more. Let's look at the `sass` task as an example. Although it generates CSSes based on\nconfigured `:source-path`, as all other tasks this one also accepts an argument which can be one of following types:\n\n - A keyword. This type of arguments is automatically handled by _revolt_. As for now only `:help` responds - returns a human readable description of given task.\n - A `java.nio.file.Path`. This type of arguments is also automatically handled by _revolt_ and is considered as a notification that particular file has been changed and task should react upon. \n `sass` task uses path to filter already configured `:resources` and rebuilds only a subset of SCSSes (if possible).\n - A map. Here it's up to task how to handle this kind of argument, by convension _revolt_ simply merges provided map into existing configuration:\n\n``` clojure\n(info {:environment :testing})\n⇒ {:name \"foo\", :package \"bar.bazz\", :version \"0.0.1\", :description \"My awesome project\", :environment :testing}\n```\n\nSometimes, in particular when tasks are composed together, it may be useful to provide argument with help of `partial` function:\n\n``` clojure\n(def build (comp capsule cljs sass (partial info {:environment :testing}) clean))\n```\n\nThis way we can tinker with our builds and packaging in a REPL without changing a single line of base configuration. Eg. to generate a thin capsule (where dependencies\nwill be fetched on first run) with an heavy-optimized version of our clojurescripts, we can construct a following pipeline:\n\n``` clojure\n(def build (comp (partial capsule {:capsule-type :thin})\n                 (partial cljs {:compiler {:optimizations :advanced}})\n                 sass\n                 info\n                 clean))\n```\nNow, as we know already how tasks work in general and how additional argument may extend or alter their base configuration, the question is how can we get these precious tasks into our hands?\n\nWell, quite easy actually. As mentioned before, tasks are denoted by qualified keywords like `:revolt.task/capsule`. All we need is now to _require-a-task_ :\n\n``` clojure\n(require '[revolt.task :as t])  ;; we need a task namespace first\n(t/require-task ::t/capsule)    ;; now we can require specific task\n\n(capsule)                       ;; task has been interned into current namespace\n⇒ {:uberjar \"dist/foo.jar\"}\n```\n\nIndeed, `require-task` is a macro which does the magic, it loads and interns into current namespace required task. It's also possible to intern a task with different name:\n\n``` clojure\n(t/require-task ::t/capsule :as caps)  ;; note the \":as caps\" here\n\n(caps)\n⇒ {:uberjar \"dist/foo.jar\"}\n```\n\nor to save unnecessary typing and load a bunch of tasks at once with `require-all` macro:\n\n``` clojure\n(t/require-all [::t/clean ::t/cljs ::t/sass ::t/capsule ::t/aot ::t/info])\n(def build (comp capsule cljs sass info clean))\n\n(build)\n⇒ { final context is returned here }\n```\n\n`require-task` and `require-all` are simple ways to dynamically load tasks we want to play with and by chance turn our REPLs into training ground where all tasks are impatiently waiting to be\nused and abused :)\n\nFollowing is the complete list of built-in tasks:\n\n| task   | description                        | parameters                                                                                   |\n|--------|------------------------------------|----------------------------------------------------------------------------------------------|\n| clean  | cleans target directory            |`:extra-paths` collection of additional paths to clean                                       |\n| sass   | converts sass/scss assets into CSS |`:source-path`  directory with sass/scss resources to transform\u003cbr\u003e`:output-path` directory where to store generated CSSes\u003cbr\u003e`:sass-options` additional sass-compiler options:\u003cbr\u003e⇒ `:source-map` (bool) Enable source-maps for compiled CSS\u003cbr\u003e⇒ `:output-style` :nested, :compact, :expanded or :compressed|\n| assets | fingerprints static assets like images, scripts or styles |`:assets-path` collection of paths with assets to fingerprint\u003cbr\u003e`:exclude-paths` collection of paths to exclude from fingerprinting\u003cbr\u003e`:update-with-exts` extensions of files to update with new references to fingerprinted assets\u003cbr\u003e\u003cbr\u003eBy default all javascripts, stylesheets and HTML resources are scanned for references to fingerprinted assets. Any recognized reference is being replaced with fingerprinted version.|\n| aot    | Ahead-Of-Time compilation          |`:extra-namespaces` collection of additional namespaces to compile                             |\n| jar    | JAR file packager                  |`:exclude-paths` collection of paths to exclude from jar package\u003cbr\u003e`:output-jar` path to output jar (dist/\u003cname\u003e-\u003cversion\u003e.jar by default) |\n| cljs   | clojurescript compiler             |`:compiler` global clojurescript [compiler options](https://clojurescript.org/reference/compiler-options) used for all builds\u003cbr\u003e`:builds` collection of builds, where each build consists of:\u003cbr\u003e⇒ `:id` build identifier\u003cbr\u003e⇒ `:source-paths` project-relative path of clojurescript files to compile\u003cbr\u003e⇒ `:compiler` - clojurescript [compiler options](https://clojurescript.org/reference/compiler-options)|\n| test   | clojure.test runner                |`:notify` enable sound notification? (defaults to true)\u003cbr\u003e`:parallel` run tests in parallel? (defaults to false)\u003cbr\u003e`:test-matcher` regex to select test ns-es (defaults to #\\\".*test\\\")\u003cbr\u003e`:report` reporting function (:pretty, :\u003czero-width space\u003eprogress or :junit)\u003cbr\u003e`:filter` fn to filter the test vars\u003cbr\u003e`:on-start` fn to call before running tests (after reloading namespaces)\u003cbr\u003e`:on-end` fn to call after running tests\u003cbr\u003e`:cloverage` enable Cloverage coverage report? (defaults to false)\u003cbr\u003e`:cloverage-opts` Cloverage options (defaults to nil)|\n| codox  | API documentation generator        |`:name` project name, eg. \"edge\"\u003cbr\u003e`:package` symbol describing project package, eg. defunkt.edge\u003cbr\u003e`:version` project version, eg. \"1.2.0\"\u003cbr\u003e`:description`  project description to be shown\u003cbr\u003e`:namespaces` collection of ns-es to document (by default all ns-es)|\n| info   | generates project information      |`:name` project name, eg. \"edge\"\u003cbr\u003e`:package` symbol describing project package, eg. defunkt.edge\u003cbr\u003e`:version` project version, eg. \"1.2.0\"|\n|capsule | generates an uberjar-like capsule  |`:capsule-type` capsule type: `:empty`, `:thin` or `:fat` (defaults to `:fat`)\u003cbr\u003e`:exclude-paths` collection of project paths to exclude from capsule\u003cbr\u003e`:output-jar` project related path of output jar, eg. dist/foo.jar\u003cbr\u003e`:main` - main class to be run\u003cbr\u003e\u003cbr\u003e[Capsule options](http://www.capsule.io/reference):\u003cbr\u003e  `:min-java-version`\u003cbr\u003e `:min-update-version`\u003cbr\u003e `:java-version`\u003cbr\u003e `:jdk-required?`\u003cbr\u003e  `:jvm-args`\u003cbr\u003e  `:environment-variables`\u003cbr\u003e  `:system-properties`\u003cbr\u003e  `:security-manager`\u003cbr\u003e  `:security-policy`\u003cbr\u003e  `:security-policy-appended`\u003cbr\u003e  `:java-agents`\u003cbr\u003e `:native-agents`\u003cbr\u003e  `:native-dependencies`\u003cbr\u003e  `:capsule-log-level`|\n\n\n## Usage\n\nOk, so having now both plugins and tasks at our disposal, let's get back to the question how `clj` tool can make use of these toys. Clj comes with a nice mechanism of `aliases` which allows to specify\nat command line additional dependencies or classpaths to be resolved when application starts up. Let's add few aliases to group dependencies based on tools required during development time:\nAssuming clojurescript, nrepl and capsule for packaging as base tools being used, this is all we need in `deps.edn`: \n\n``` clojure\n{:aliases {:dev {:extra-deps  {defunkt/revolt {:mvn/version \"1.3.0\"}}\n                 :extra-paths [\"target/assets\"]\n                 :main-opts   [\"-m\" \"revolt.bootstrap\"\n                               \"-p\" \"nrepl,rebel\"]}\n\n           ;; dependencies for nrepl\n           :dev/nrepl {:extra-deps {cider/cider-nrepl {:mvn/version \"0.21.0\"}\n                                    refactor-nrepl {:mvn/version \"2.4.0\"}}}\n\n           ;; dependencies for clojurescript\n           :dev/cljs {:extra-deps {org.clojure/clojurescript {:mvn/version \"1.10.238\"}\n                                   binaryage/devtools {:mvn/version \"0.9.9\"}\n                                   com.bhauman/figwheel-main {:mvn/version \"0.1.9-SNAPSHOT\"}\n                                   re-frame {:mvn/version \"0.10.5\"}\n                                   reagent {:mvn/version \"0.8.0-alpha2\"}}}\n\n           ;; dependencies for packaging tasks\n           :dev/pack {:extra-deps {co.paralleluniverse/capsule {:mvn/version \"1.0.3\"}\n                                   co.paralleluniverse/capsule-maven {:mvn/version \"1.0.3\"}}}}}\n```\n\nNote the `:extra-paths` and `:main-opts`. First one declares additional class path - a _target/assets_ directory where certain tasks (eg. sass, cljs, aot) dump their generated assets like CSSes or compiled clojurescripts.\nThis is required only to keep things auto-reloadable - application needs to be aware of resources being (re)generated.\n\n`:main-opts` on the other hand are the parameters that `clj` will use to bootstrap revolt: `-m revolt.bootstrap` instructs `clj` to use `revolt.bootstrap` namespace as a main class and pass rest of parameters over there. \n\nHere is the complete list of all accepted parameters:\n\n    -c, --config     : location of configuration resource.\n\n    -d, --target-dir : location of target directory (relative to project location). This is where all re/generated\n                       assets are being stored. Defaults to \"target\".\n\n    -p, --plugins    : comma separated list of plugins to activate. Each plugin (a stringified keyword) may be\n                       specified with optional parameters:\n    \n                          clojure -A:dev:dev/nrepl:dev/cljs:dev/pack -p revolt.task/nrepl,revolt.task/rebel:init-ns=revolt.task\n                      \n    -t, --tasks      : comma separated list of tasks to run. Simmilar to --plugins, each task (a stringified keyword)\n                       may be specified with optional parameters:\n    \n                          clojure -A:dev:dev/nrepl:dev/cljs:dev/pack -t revolt.plugin/clean,revolt.plugin/info:env=test:version=1.1.2\n                      \n\nTo make things even easier to type, namespace parts of qualified keywords may be omitted when a built-in task or plugin is being used. So, it's perfectly valid to call:\n\n                          clojure -A:dev:dev/nrepl:dev/cljs:dev/pack -t clean,info:env=test:version=1.1.2\n\nLast thing to add - when running _revolt_ both with `--plugins` and `--tasks` the latter takes precedence, which means that plugins get activated once all required tasks finish their work.\n\n## Development and more tech details\n\nThis is to describe in details of how plugins and task are loaded and initialized and provide a simple guide to develop own extensions.\n\n### Plugins rediscovered\n\nWhen _revolt_ starts up, it collects all the keywords listed in `--plugins` and sequentially loads corresponding namespaces calling a `create-plugin` multi-method (with keywords themselves as a dispatch values) at the end.\nEvery such a function returns an object which extends a `Plugin` protocol:\n\n```clojure\n    (defprotocol Plugin\n      (activate [this ctx] \"Activates plugin within given context\")\n      (deactivate [this ret] \"Deactivates plugin\"))\n```\n\nThis simple mechanism allows to create a new plugin just like that:\n\n```clojure\n    (ns defunkt.foo\n      (:require [revolt.plugin :refer [Plugin create-plugin]]))\n      \n    (defmulti create-plugin ::bar [_ config]\n      (reify Plugin\n        (activate [this ctx] ...)\n        (deactivate [this ret] ...)))\n```\n\nAnd configure it later as follows:\n    \n    {:defunkt.foo/bar {:bazz 1}}\n    \nEach plugin gets a _session context_ during activation phase. Context contains all the crucial stuff that most of plugins base on:\n\n```clojure\n    (defprotocol SessionContext\n      (classpaths [this]   \"Returns project classpaths.\")\n      (target-dir [this]   \"Returns a project target directory.\")\n      (config-val [this k] \"Returns a value from configuration map.\"))\n```\n\nNote that plugin activation should return a value required to its correct deactivation. This value will be passed later to `deactivate` function as `ret`.\n\n### Tasks rediscovered\n\ntbd.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmbuczko%2Frevolt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmbuczko%2Frevolt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmbuczko%2Frevolt/lists"}