clj-reload

repository·main·Indexed 43 days ago

https://github.com/tonsky/clj-reload

A Clojure library for smarter code reloading that tracks namespace dependencies and reloads them in topological order. It features selective reloading, lifecycle hooks for managing stateful resources, and the ability to persist specific vars using ^:clj-reload/keep metadata. Version 1.0.0.

Tokens
2K
Snippets
10
Records
16
Agent score
5%

What's inside clj-reload

  1. Compare clj-reload with tools.namespace

    main

    While both clj-reload and tools.namespace track file modification times and reload namespaces in topological order, clj-reload provides several advantages for a smoother development workflow:

    • Selective Reloading: Unlike tools.namespace which reloads every namespace it finds, clj-reload only reloads namespaces that were already loaded. This prevents experimental or broken files from breaking your workflow.
    • Efficient First Reload: The very first reload in clj-reload only reloads files that have actually changed, whereas tools.namespace reloads everything on the first pass.
    • Advanced Dependency Tracking: Supports namespaces split across multiple files and can detect dependencies in top-level standalone require and use forms.
    • Lifecycle Hooks: Supports per-namespace load and unload hooks, allowing you to manage state more precisely than tools.namespace.
    • State Preservation: Can keep individual vars around and restore previous values after a reload (making it compatible with patterns like defonce).
    • Configuration: Allows specifying exclusions during configuration without modifying the source code of the namespaces being excluded.
    • Lightweight: Has a significantly smaller codebase and zero runtime dependencies.
  2. Warning: Avoid holding onto old references

    main

    Because clj-reload unloads entire namespaces, any existing references to vars or aliased namespaces will point to the old, unloaded versions after a reload.

    Best Practices to avoid stale references:

    1. Do not put namespaces you intend to reload in the :require section of your REPL namespace.
    2. Instead of using direct symbols (e.g., full.ns/sym) or atoms (e.g., @#'full.ns/sym), use (resolve 'full.ns/sym) to fetch the current version of the var after a reload.
  3. Compare clj-reload with Clojure's :reload-all

    main

    Using Clojure's built-in (require 'namespace :reload-all) is often insufficient for complex development because:

    1. Excessive Reloading: It reloads all upstream dependencies regardless of whether they actually changed.
    2. Incomplete Dependency Tracking: It does not track downstream dependencies. If you reload b, it will not trigger a reload of a (which depends on b), only c (which might be an upstream dependency).
    3. State Issues: It "evals on top" of existing definitions, similar to buffer evaluation, which can lead to inconsistent state.
  4. Use unload and reload hooks

    main

    To manage stateful resources (like web servers or database connections) during a reload, you can define hooks:

    • Unload Hook: A function called before a namespace is unloaded. The default name is before-ns-unload. You can customize this name using :unload-hook in reload/init.
    • Reload Hook: A function called after a namespace is reloaded. Customize this using :reload-hook in reload/init (default name is after-ns-reload).
    ;; Default hook usage
    (defn before-ns-unload []
      (server/stop my-server))
    
    ;; Custom hook configuration
    (reload/init
      {:dirs ["src"]
       :unload-hook 'my-unload})
  5. Keep variables between reloads with ^:clj-reload/keep

    main

    Normally, reloading a namespace removes all its vars. To persist specific forms between reloads, annotate them with the ^:clj-reload/keep metadata. This works for def, defn, deftype, defrecord, and defprotocol.

    Note: defonce works automatically without this metadata.

    (ns test)
    
    (defonce x (rand-int 1000))
    
    ^:clj-reload/keep
    (def y (rand-int 1000))
    
    ^:clj-reload/keep
    (defrecord Z [])
  6. Use the init-less workflow

    main

    If you want to use clj-reload without an explicit init call (e.g., in a system-wide profile), you can:

    1. Skip init. clj-reload will attempt to initialize with every directory on the classpath.
    2. Use namespace metadata: (ns ^:clj-reload/no-reload my.ns).

    To disable the automatic initialization, set the environment variable CLJ_RELOAD_AUTO_INIT=false or the system property clj-reload.auto-init=false.

  7. Install clj-reload

    main

    Add clj-reload as a dependency to your project. It is recommended to add it to your dev profile in deps.edn so it doesn't ship to production.

    {:aliases
     {:dev
      {:extra-deps
       {io.github.tonsky/clj-reload {:mvn/version "1.0.0"}}}}}
  8. Prevent namespaces from reloading or unloading

    main

    Use the following options in reload/init to manage stateful namespaces:

    • :no-reload: A set of namespaces that should never be reloaded. This implies :no-unload (they won't be unloaded either).
    • :no-unload: A set of namespaces that should be reloaded but never unloaded (useful for namespaces containing defonce state).
    (reload/init
      {:dirs ["src"]
       :no-reload '#{user myapp.state ...}})
  9. Control reload output verbosity

    main

    Configure the logging level during reload/init using the :output key:

    • :verbose: Logs every unloading and loading action.
    • :quieter: (Default) Logs a summary of reloaded namespaces.
    • :quiet: Produces no output.
    (reload/init {:output :verbose})
    (reload/init {:output :quiet})
  10. Reload custom file types

    main

    You can instruct clj-reload to monitor and reload files with extensions other than .clj or .cljc by providing a regular expression to the :files key in reload/init.

    (reload/init
      {:dirs ["src"]
       :files #".*[.](clj|cljc|repl)"})
  11. Handle reload errors

    main

    If a reload fails due to a compilation error, you can:

    1. Fix the error and call (reload/reload) again.
    2. Call (reload/unload) to unload the changed code without attempting to load it back. Fix the code, then call reload to restore the namespaces.