0
votes

I'm considering moving a project from boot to Gradle with clojurephant hoping to leverage more of the Gradle ecosystem. This project builds one large uberjar that contains a Clojure project with Ring and Jetty that in turn ships a ClojureScript app built with re-frame.

In boot, I essentially just required boot-cljs, added

(boot-cljs/cljs :optimizations :advanced)

to my build task, which also calls (pom), (aot) and (uber) (all standard boot tasks), and everything worked smoothly.

With Clojurephant, I find that the Clojure and ClojureScript parts end up in different subdirectories. In particular I find underneath of build

  • clojure/main
  • clojurescript/main
  • resources/main (essentially a copy of my resources project folder)

Adding to my confusion is that these paths don't translate in a way that I can see to the structure of the Uberjar that Gradle builds using the shadow plugin

Some excerpts from my build.gradle:

plugins {
    id 'dev.clojurephant.clojure' version '0.5.0'
    id 'dev.clojurephant.clojurescript' version '0.5.0'
    id 'application'
    id 'com.github.johnrengelman.shadow' version '5.0.0'
    id 'maven-publish'
    id 'distribution'
    id 'com.meiuwa.gradle.sass' version '2.0.0'
}
// ...
clojure {
  builds {
    main {
      aotAll()
    }
  }
}
// ...
clojurescript {
  builds {
    all {
      compiler {
        outputTo = 'public/app.js'
        outputDir = 'public/js/out'
        main = 'com.example.mycljsstuff'
        assetPath = 'js/out'
      }
    }
    main {
      compiler {
        optimizations = 'advanced'
        sourceMap = 'public/app.js.map'
      }
    }
    dev {
      compiler {
        optimizations = 'none'
        preloads = ['com.example.mycljsstuff']
      }
    }
  }
}

EDIT: Forgot to mention that for boot I configure the init function to start loading the CLJS code in a file called app.cljs.edn. With Clojurephant I only found a way to set a main namespace, not a function.

My question here is ultimately, how can I configure the ClojureScript build so that it works in the end when being delivered from the Uberjar?

The Clojure things seem to work. Ring and Jetty run and happily deliver a first static webpage. But all the CLJS/JS things can't be found.

I'd be super happy to just receive some pointers to other projects where I can learn, documentation, or tutorials. I haven't found a lot and then got lost in understanding the code of Clojurephant itself.

2
Heads up: I'm making progress and will share a solution should I be able to solve it. - Stefan Kamphausen

2 Answers

1
votes

A year ago at work I was able to split up a combined CLJ & CLJS (backend/frontend) project into 2 separate projects: pure Clojure for the backend, and pure ClojureScript for the frontend. This resolved many, many problems we had and I would never try to keep two codebases in the same project again.

  • The backend CLJ part continued to use Lein as the build tool. It's not perfect but it is well understood.
  • The frontend CLJS part was transitioned from the original Figwheel (aka "1.0") to the newer Figwheel-Main (aka "2.0"). Following the lead from figwheel.org we chose to restructure the build into using Deps/CLI (the original combined project used Lein for everything). The transition away from Lein to Deps/CLI was a real winner for CLJS work.

While Deps/CLI works great for pure Clojure code, be aware that it does not natively support the inclusion of Java source code. I have a template project you can clone that shows a simple workaround for this.

For any CLJS project, I highly recommend the use of Figwheel-Main over the original Figwheel as it is a major "2.0" type of upgrade, and will make your life much, much better.

Enjoy!

0
votes

Trying to answer my own question here, since I managed to get it running.

Clojure

plugins {
    id 'dev.clojurephant.clojure' version '0.5.0'
    id 'application'
    id 'com.github.johnrengelman.shadow' version '5.0.0'
    // ... more to come further down
}
group = 'com.example'
version = '1.0.0-SNAPSHOT'
targetCompatibility = 1.8
mainClassName = 'com.example.myproject'

dependencies {
    implementation(
        'org.clojure:clojure:1.10.1',
        'ring:ring:1.8.0',
        // and many more
    )
    testImplementation(
        'junit:junit:4.12',
        'clj-http-fake:clj-http-fake:1.0.3',
        'ring:ring-mock:0.4.0'
    )
    devImplementation(
        'org.clojure:tools.namespace:0.3.0-alpha4',
        'cider:cider-nrepl:0.21.1',
        'org.clojure:java.classpath',
        'jonase:eastwood:0.3.11',
        'lein-bikeshed:lein-bikeshed:0.5.2'
    )
}

clojure {
  builds {
    main {
      aotAll()
    }
  }
}

clojureRepl {
  handler = 'cider.nrepl/cider-nrepl-handler'
}

This is enough to get an executable JAR running -main from com.example.myproject namespace when calling ./gradlew shadowJar from the command line. Not sure if the application plugin is relevant here. Also, ./gradlew clojureRepl spins up an nrepl that Emacs/Cider can connect to.

ClojureScript

// Merge with plugins above. Reproduced only the CLJS relevant
// part here
plugins {
    id 'dev.clojurephant.clojurescript' version '0.5.0'
}

// Again, merge with dependencies above
dependencies {
    implementation(
        // ....
        'org.clojure:clojurescript:1.10.597',
        're-frame:re-frame:0.10.5',
        'reagent:reagent:0.7.0',
        // and a few more
    )
}

clojurescript {
    builds {
        all {
            compiler {
                outputTo = 'public/app.js'
                outputDir = 'public/state/'
                main = 'com.example.myproject.webui'
                assetPath = 'status/app.out'
            }
        }
        main {
            compiler {
                optimizations = 'advanced'
                sourceMap = 'public/app.js.map'
            }
        }
        dev {
            compiler {
                optimizations = 'none'
                preloads = ['com.example.myproject.webui']
            }
        }
    }
}

This creates a /public folder in the top level of there JAR and app.js inside that folder, which is where the HTML file delivered by Ring expects it.

One important step for me was to call my init CLJS function in the CLJS file which was taken care of by some other component before. I'm not sure that this set up is totally correct and will do the figwheel setup eventually. Maybe the call to init will not be necessary then.

CSS

// Merge ....
plugins {
    id 'com.meiuwa.gradle.sass' version '2.0.0'
}
sassCompile {
    output = file("$buildDir/resources/main/public/")
    source = fileTree("${rootDir}/src/main/resources/public/")
    include("**/*.scss")
    exclude("**/_*.sass", "**/_*.scss")
}

This compiles my app.scss to app.css in the right spot, where my HTML file searches for it.

Pro & Con

After migrating, I get for free

  • Faster compilation locally and in CI after setting up the caches correctly.
  • License and OWASP dep check reports by using plugins com.github.jk1.dependency-license-report and org.owasp.dependencycheck for which equivalents exists in leiningen but not boot (AFAIK).
  • Maven publishing with authentication via HTTP header instead of username/password which is not available in boot.

On the downsides:

  • Yucky syntax in my build file. No, really.
  • I have to enter the nrepl port number in Emacs manually.