Thanks for joining to the discussion. I don’t know what is better or not, I’m more at the exploring the problem space and the solution space at this stage.
I think that file is maybe a good example of where the Elixir pattern comes in handy. Or the Next.JS pattern, or some more inheritance based pattern. Let me brainstorm a bit here.
In your case, how do you update that file for users? Like if I created a webapp with Biff 1.1, and now in Biff 1.6 that file is drastically evolved?
Am I expected to manually copy/paste the new one over it in my project? Or do some kind of pull merge?
What if I had customized something in that file? I want the new functionality of that file from 1.6, but I don’t want to lose my customization and would still like it applied to 1.6’s version of it.
That’s one area where I think this pattern could help, in that the user’s example.clj would be empty except for inheriting from the framework’s app.clj or wtv it is called. And if the user wants to customize things, just redef whatever var or function you want in the user’s example.clj. When you updgrade Biff, you’d always have the most up to date version of example.clj, with all your re-defs automatically applied back.
Another aspect is in your features, now I don’t know if it’s as useful, but if someone did:
(ns myfeature
(:require [inheritance :refer [inherit]]))
(inherit biff.feature)
They could have all the requires available, and without doing anything more, this could be fully functioning feature. They could and try to run: url:port/myfeatrue
and see a “Hello World”.
The macro would have done:
(require '[com.biffweb :as biff])
(require '[com.myapp.middleware :as mid])
(require '[com.myapp.ui :as ui])
(require '[com.myapp.util :as util])
(defn myfeature [sys]
[:html
[:head
[:script {:src "https://unpkg.com/htmx.org@1.6.1"}]]
[:body
[:div "Welcome to your myfeature page!"]]]))
(def features
{:routes ["" {:middleware [mid/wrap-default]}
["/" {:get myfeature}]]})
Also, one thing Pheonix does, is that it double the inheritance, it scaffolds people’s project to inherit from a user base and that to inherit from the framework. So its more like
(ns com.myapp
(:require [inheritance :refer [inherit]]))
(inherit biff.app)
;; User overrides go below
(ns com.myapp.feature
(:require [inheritance :refer [inherit binded-quote]]))
(defmacro __inherit__[& {:as opts}]
(binded-quote
[opts opts]
(inherit biff.feature opts)
;; User templates go below here
))
(ns com.myapp.features.home
(:require [inheritance :refer [inherit]]))
(inherit com.myapp.feature {})
So the user can also add common functionality of their own into all their own features by adding to their com.myapp.myfeature
inherit template.
And the opts
map can be used to customize the template.
I don’t think any of this is necessary, but I think it can be used to add a certain level of productivity to get started, and ease of use. It basically lets you templatize the common namespaces you think your users are going to be writing themselves, where there’s a nice path to “upgrading” the template, and a nice path to overriding only parts of it.
P.S.: Really like what I saw of Biff otherwise. I think the secret sauce of Biff is the choice of XTDB and HTMX. Those two choices considerably simplifies the challenges, because they work so well with Clojure semantics. Anything using a traditional RDBMS like MySQL you face a big challenge of migrations, mapping tables to entities, and back, connection pooling, and all that. And if you go down the JS path, now you’ve got another sub-project for your ClojureScript, React, all that stuff. But also how you pulled things together seems quite nice, and I like that you included deploy scripts, hot-reload, and all that. It looks like a pretty good start.