I recently posted this Clojurians Slack re-frame question:
I have a deps.edn/figwheel-main re-frame project that I'm trying to add re-frame-10x to. The deps.edn (day8.re-frame/re-frame-10x {:mvn/version "1.5.0"}) and dev.cjls.edn (:preloads [day8.re-frame-10x.preload]) seem correct and the project builds without errors. When the web app is started though, I get this run-time error:
Uncaught Error: Bad dependency path or symbol: highlight.js.lib.core
I and others have also seen this type of build-time error:
No such namespace: highlight.js/lib/core, could not locate highlight/js_SLASH_lib_SLASH_core.cljs, highlight/js_SLASH_lib_SLASH_core.cljc, or JavaScript source providing "highlight.js/lib/core" (Please check that namespaces with dashes use underscores in the ClojureScript file name) in file target/public/cljs-out/dev/re_highlight/core.cljs
Both errors are originating from re-highlight/core.cljs:
1 2 3 4 5 6 7 8 |
(ns re-highlight.core (:require [goog.object :as gobj] [reagent.core :as r] [reagent.dom :as rdom] ["highlight.js/lib/core" :as hljs] ["highlight.js/lib/languages/clojure" :as clojure])) ... |
re-highlight (a re-frame-10x dependency) is built with shadow-cljs while the re-frame project is built with lein/deps.edn/figwheel-main. The re-frame project does not have a dependency on either re-highlight or highlight.js. The challenge here is providing the highlight.js (an NPM library) dependencies to re-highlight.
There are ways to include NPM libraries in ClojureScript projects, but each is specific to a particular build system:
- ClojureScript is not an Island: Integrating Node Modules
- ClojureScript with Webpack
- figwheel-main: Using NPM
- shadow-cljs: NPM
This cross-build system situation seems unique though. Providing the highlight.js
library to re-highlight
turned out to be an eye-opening deep dive into ClojureScript build systems. See the Commentary section at the end of this post.
Long story short, I was able to find a solution for exposing NPM libraries to shadow-cljs projects included in a deps.edn build.
It involves creating a Javascript bundle containing the needed NPM dependency (highlight.js) using webpack and then "manually" making it available to re-highlight.
Here is a step-by-step guide for adding re-frame-10x to a deps.edn re-reframe project.
First, create package.json with the needed dependencies.
1 2 3 4 5 6 |
npm install highlight.js --save npm install webpack webpack-cli --save-dev # Manually add this to package.json: # "scripts": { # "build": "webpack --mode=development" # }, |
Final package.json:
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "scripts": { "build": "webpack --mode=development" }, "dependencies": { "highlight.js": "^11.5.1" }, "devDependencies": { "webpack": "^5.74.0", "webpack-cli": "^4.10.0" } } |
Create src/js/main.js with these contents. The require()
statements are needed so webpack will include the NPM libraries in the output bundle.
1 2 |
require('highlight.js') require('highlight.js/lib/languages/clojure') |
Create webpack.config.js with these contents:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const webpack = require('webpack'); const path = require('path'); const BUILD_DIR = path.resolve(__dirname, 'resources', 'public', 'js', 'compiled'); const APP_DIR = path.resolve(__dirname, 'src', 'js'); const config = { entry: `${APP_DIR}/main.js`, output: { path: BUILD_DIR, filename: 'bundle.js' } }; module.exports = config; |
Add the following lines before the complied app.js
in resources/public/index.html.
1 2 3 4 5 6 7 |
... <script src="js/compiled/out/goog/base.js" type="text/javascript"></script> <script> goog.addDependency("../../bundle.js", ['highlight.js.lib.core', 'highlight.js.lib.languages.clojure'], []); </script> <script src="js/compiled/app.js" type="text/javascript"></script> ... |
This is where the magic is happening.
- The highlight.js dependencies are being manually added with
goog.addDependency()
before the rest of the application dependencies are loaded. - The Clojurescript compiler-generated app.js has the following code. We're just loading goog/base.js first so the
goog
functions are defined.
1 2 3 |
... if(typeof goog == "undefined") document.write('<script src="js/compiled/out/goog/base.js"></script>'); ... |
Note: The paths and JS build file names (e.g. app.js
) above may not match your specific project structure. If so, they would need to be adjusted accordingly.
Add re-frame-10x dependencies to deps.edn:
1 2 3 4 5 |
... day8.re-frame/tracing {:mvn/version "0.6.2"} day8.re-frame/test {:mvn/version "0.1.5"} day8.re-frame/re-frame-10x {:mvn/version "1.5.0"} ... |
The dev.cljs.edn file needs the following so re-frame-10x is loaded properly:
1 2 3 4 5 6 |
... :closure-defines { "goog.DEBUG" true "re_frame.trace.trace_enabled_QMARK_" true} :preloads [day8.re-frame-10x.preload] ... |
Run the project:
1 2 3 4 |
npm run build # Creates bundle.js lein repl # or lein figwheel |
Voilà! The highlight.js dependency in re-highlight is satisfied and re-frame-10x runs as expected.
There must be a better way to do this. I just couldn't find it...
Commentary
This solution seems rather hacky and took way too long to discover. I can't tell you the number of rabbit holes I went down with the NPM inclusion methods listed above. Each of them just uncovered further dependency and configuration issues or would have resulted in undesirable refactoring. The code base I'm working with is rather large and I didn't want to completely change the build system just to add a development tool.
To be honest, the CLJ/CLJS build tools and their cross-pollinated dependency systems (lein, shadown-cljs, etc.) are very confusing. There is no idiomatic/standard way to build Clojure(Script) projects. Everyone is using a different combination or permutation of build systems. Also, the clojure/clj CLI and tooling just plain suck. I think these things are a real barrier to Clojure(Script) adoption.