Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions libs/kit-generator/.dir-locals.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
;;; Directory Local Variables -*- no-byte-compile: t -*-
;;; For more information see (info "(emacs) Directory Variables")

((nil . ((cider-jack-in-cmd . "clojure -M:dev:cider")
(cider-repl-init-code . ("(ns user)")))))
22 changes: 13 additions & 9 deletions libs/kit-generator/deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@
mvxcvi/cljstyle {:mvn/version "0.17.642"}
cljfmt/cljfmt {:mvn/version "0.9.2"}
clj-fuzzy/clj-fuzzy {:mvn/version "0.4.1"}
clojure-deep-merge/clojure-deep-merge {:mvn/version "0.1.2"}}
:aliases {:cider
clojure-deep-merge/clojure-deep-merge {:mvn/version "0.1.2"}
babashka/process {:mvn/version "0.6.23"}
com.stuartsierra/dependency {:mvn/version "1.0.0"}}
:aliases {:dev
{:extra-paths ["test"]
:extra-deps {nubank/matcher-combinators {:mvn/version "3.9.2"}}}
:cider
{:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}
cider/cider-nrepl {:mvn/version "0.58.0"}}
:main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]" "-i"]}

:test
{:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}
babashka/fs {:mvn/version "0.5.27"}
babashka/process {:mvn/version "0.6.23"}}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test}}}
{:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}
nubank/matcher-combinators {:mvn/version "3.9.2"}
babashka/fs {:mvn/version "0.5.27"}}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test}}}
197 changes: 145 additions & 52 deletions libs/kit-generator/src/kit/api.clj
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
(ns kit.api
"Public API for kit-generator."
(:require
[clojure.string :as str]
[clojure.test :as t]
[kit.generator.hooks :as hooks]
[kit.generator.io :as io]
[kit.generator.modules :as modules]
[kit.generator.modules-log :refer [track-installation installed-modules
module-installed?]]
[kit.generator.modules.dependencies :as deps]
[kit.generator.modules.generator :as generator]
[kit.generator.snippets :as snippets]
[clojure.test :as t]))
[kit.generator.snippets :as snippets]))

;; TODO: Add docstrings

(def default-edn "kit.edn")

(defn- read-ctx
[path]
(assert (not (str/blank? path)))
(-> path
(slurp)
(io/str->edn)))
(defn read-ctx
([]
(read-ctx default-edn))
([path]
(assert (not (str/blank? path)))
(-> path
(slurp)
(io/str->edn))))

(defn- log-install-dependency [module-key feature-flag deps]
(print "Installing module" module-key)
Expand All @@ -30,36 +36,13 @@
(defn- log-missing-module [module-key]
(println "ERROR: no module found with name:" module-key))

(defn- install-dependency
"Installs a module and its dependencies recursively. Asumes ctx has loaded :modules.
Note that `opts` have a different schema than the one passed to `install-module`,
the latter being preserved for backwards compatibility. Here `opts` is a map of
module-key to module-specific options.

For example, let's say `:html` is the main module. It would still be on the same level
as `:auth`, its dependency:

```clojure
{:html {:feature-flag :default}
:auth {:feature-flag :oauth}}
```

See flat-module-options for more details."
[{:keys [modules] :as ctx} module-key opts]
(if (modules/module-exists? ctx module-key)
(let [{:keys [module-config]} (generator/read-module-config ctx modules module-key)
{:keys [feature-flag] :or {feature-flag :default} :as module-opts} (get opts module-key {})
deps (deps/resolve-dependencies module-config feature-flag)]
(log-install-dependency module-key feature-flag deps)
(doseq [module-key deps]
(install-dependency ctx module-key opts))
(generator/generate ctx module-key module-opts))
(log-missing-module module-key))
:done)

(defn- flat-module-options
"Converts options map passed to install-module into a flat map
of module-key to module-specific options."
"Converts options map passed to install-module into a flat map of module-key to
module-specific options. A module-specific option is an option that will be
applied to the primary module, identified by module-key, when installing that
module. For example, `:feature-flag` is a module-specific option, while
`:dry?` is not, because the latter applies to the installation process as a
whole. See the test below for examples."
{:test (fn []
(t/are [opts module-key output] (flat-module-options opts module-key)
{} :meta {}
Expand All @@ -77,37 +60,147 @@
(defn sync-modules
"Downloads modules for the current project."
[]
(modules/sync-modules! (read-ctx default-edn))
(modules/sync-modules! (read-ctx))
:done)

(defn list-modules
"List modules available for the current project."
[]
(let [ctx (modules/load-modules (read-ctx default-edn))]
(let [ctx (modules/load-modules (read-ctx))]
(modules/list-modules ctx))
:done)

(defn- report-install-module-error
[module-key e]
(println "ERROR: Failed to install module" module-key)
(.printStackTrace e))

(defn- report-install-module-success
[module-key {:keys [success-message require-restart?]}]
(println (or success-message
(str "module " module-key " installed successfully!")))
(when require-restart?
(println "restart required!")))

(defn- report-already-installed
[installed-modules]
(doseq [{:module/keys [key]} installed-modules]
(println "WARNING: Module" key "was already installed successfully. Skipping installation.")))

(defn installation-plan
"Loads and resolves modules in preparation for installation, as
well as determining which modules are already installed vs which
need to be installed."
[module-key kit-edn-path opts]
(let [opts (flat-module-options opts module-key)
ctx (modules/load-modules (read-ctx kit-edn-path) opts)
{installed true pending false} (->> (deps/dependency-list ctx module-key opts)
(group-by #(module-installed? ctx (:module/key %))))]
{:ctx ctx
:installed-modules installed
:pending-modules pending
:opts opts}))

(defn print-installation-plan
"Prints a detailed installation plan for a module and its dependencies."
([module-key]
(print-installation-plan module-key {:feature-flag :default}))
([module-key opts]
(print-installation-plan module-key "kit.edn" opts))
([module-key kit-edn-path opts]
(let [{:keys [opts installed-modules pending-modules]} (installation-plan module-key kit-edn-path opts)]
(when (seq installed-modules)
(println "ALREADY INSTALLED (skipped)")
(doseq [{:module/keys [key]} installed-modules]
(println "" key))
(println))

(when (seq pending-modules)
(println "INSTALLATION PLAN")
(doseq [{:module/keys [key doc] :as module} pending-modules]
(let [module-feature-flag (get-in opts [key :feature-flag] :default)]
(println "" key (if (not= :default module-feature-flag)
(str "@" (name module-feature-flag))
""))
(when doc
(println " " doc))

(doseq [description (concat (generator/describe-actions module)
(hooks/describe-hooks module))]
(println " -" description))
(println))))

(println "SUMMARY")
(let [pending-count (count pending-modules)
installed-count (count installed-modules)]
(when (pos? installed-count)
(println " " installed-count "module(s) already installed (skipped)"))
(if (pos? pending-count)
(println " " pending-count "module(s) to install")
(println " " "Nothing to install!")))
(println))))

(defn- prompt-y-n-all
"Prompts the user to accept actions with a yes/no/all question.
If the user answers 'all', the accept-hooks-atom is set to true
and all subsequent calls will return true without prompting."
[prompt accept-hooks-atom]
(if (nil? @accept-hooks-atom)
(let [answers ["y" "n" "all"]]
(print prompt (str " (" (str/join "/" answers) "): "))
(loop []
(flush)
(let [response (str/trim (str/lower-case (read-line)))]
(case response
"y" true
"n" false
"all" (reset! accept-hooks-atom true)
(do (println "\nPlease answer one of:" (str/join ", " answers))
(recur))))))
@accept-hooks-atom))

(defn- prompt-run-hooks
"Prompts the user to accept running hooks defined in a module.
See prompt-y-n-all for details."
[accept-hooks-atom hooks]
(println "The following hook actions will be performed:")
(doseq [hook hooks]
(println " $" hook))
(prompt-y-n-all "Run the hook?" accept-hooks-atom))

(defn install-module
"Installs a kit module into the current project or the project specified by a
path to kit.edn file.

> NOTE: When adding new options, update flat-module-options."
> NOTE: When adding new module-specific options, update flat-module-options.
See the function for more details."
([module-key]
(install-module module-key {:feature-flag :default}))
([module-key opts]
(install-module module-key "kit.edn" opts))
([module-key kit-edn-path opts]
(let [ctx (modules/load-modules (read-ctx kit-edn-path))]
(install-dependency ctx module-key (flat-module-options opts module-key)))))
([module-key kit-edn-path {:keys [accept-hooks? dry?] :as opts}]
(if dry?
(print-installation-plan module-key kit-edn-path opts)
(let [{:keys [ctx pending-modules installed-modules]} (installation-plan module-key kit-edn-path opts)
accept-hooks-atom (atom accept-hooks?)]
(report-already-installed installed-modules)
(doseq [{:module/keys [key resolved-config] :as module} pending-modules]
(try
(track-installation ctx key
(generator/generate ctx module)
(hooks/run-hooks :post-install resolved-config
{:confirm (partial prompt-run-hooks accept-hooks-atom)})
(report-install-module-success key resolved-config))
(catch Exception e
(report-install-module-error key e))))))
:done))

(defn list-installed-modules
"Lists installed modules and modules that failed to install, for the current
project."
[]
(doseq [[id status] (-> (read-ctx default-edn)
:modules
:root
(generator/read-modules-log))]
(doseq [[id status] (-> (read-ctx)
(installed-modules))]
(println id (if (= status :success)
"installed successfully"
"failed to install")))
Expand All @@ -121,25 +214,25 @@
@db))))

(defn sync-snippets []
(let [ctx (read-ctx default-edn)]
(let [ctx (read-ctx)]
(snippets/sync-snippets! ctx)
(snippets-db ctx true)
:done))

(defn find-snippets [query]
(snippets/print-snippets (snippets-db (read-ctx default-edn)) query)
(snippets/print-snippets (snippets-db (read-ctx)) query)
:done)

(defn find-snippet-ids [query]
(println (str/join ", " (map :id (snippets/match-snippets (snippets-db (read-ctx default-edn)) query))))
(println (str/join ", " (map :id (snippets/match-snippets (snippets-db (read-ctx)) query))))
:done)

(defn list-snippets []
(println (str/join "\n" (keys (snippets-db (read-ctx default-edn)))))
(println (str/join "\n" (keys (snippets-db (read-ctx)))))
:done)

(defn snippet [id & args]
(snippets/gen-snippet (snippets-db (read-ctx default-edn)) id args))
(snippets/gen-snippet (snippets-db (read-ctx)) id args))

(comment
(t/run-tests 'kit.api))
49 changes: 49 additions & 0 deletions libs/kit-generator/src/kit/generator/features.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
(ns kit.generator.features
"Feature flag resolution based on :feature-requires."
(:require
[deep.merge :as deep-merge]))

(defn- check-feature-not-found
"Throw exception unless the feature was defined in the config."
[module-config feature-flag]
(when-not (contains? module-config feature-flag)
(throw (ex-info (str "Feature not found: " feature-flag)
{:error ::feature-not-found
:feature-flag feature-flag}))))

(defn resolve-module-config
"Return module config resolved using the feature flag and :feature-requires fields.
Handles cyclic dependencies by not following them."
[module-config feature-flag]
(let [full-config module-config
result (feature-flag module-config)]
(loop [result result module-config module-config]
(if-let [feature-requires (seq (:feature-requires result))]
(recur (apply deep-merge/concat-merge
(dissoc result :feature-requires)
(mapv #(or (get module-config %)
(check-feature-not-found full-config %)
{})
feature-requires))
(apply dissoc module-config feature-requires))
result))))

(comment (resolve-module-config
{:default {:foo :bar
:actions {:assets [:assetA]}
:hooks {:post-install [":default installed"]}
:feature-requires [:base]
:requires [:1]
:success-message ":default installed"}
:base {:baz :qux
:actions {:assets [:asset1 :asset2]}
:injections [:inj1]
:hooks {:post-install [":base post install"]}
:feature-requires [:extras]
:requires [:2]
:success-message ":base installed"}
:extras {:actions {:assets [:extra-asset1]}
:feature-requires [:default]}}
:default)
;
)
20 changes: 11 additions & 9 deletions libs/kit-generator/src/kit/generator/git.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
(ns kit.generator.git
(:require
[clojure.string :as string]
[clj-jgit.porcelain :as git])
[clj-jgit.porcelain :as git]
[clojure.java.io :as jio]
[clojure.string :as string]
[kit.generator.io :as io])
(:import
[java.io File FileNotFoundException]))
[java.io FileNotFoundException]))

(defn repo-root [name git-url]
(or name
Expand All @@ -14,10 +16,10 @@
(first))))

(defn repo-path [root name git-url]
(str root File/separator (repo-root name git-url)))
(io/concat-path root (repo-root name git-url)))

(defn git-config []
(if (.exists (clojure.java.io/file "kit.git-config.edn"))
(if (.exists (jio/file "kit.git-config.edn"))
(read-string (slurp "kit.git-config.edn"))
{:name "~/.ssh/id_rsa"}))

Expand All @@ -32,10 +34,10 @@
(git/git-pull repo))
(catch FileNotFoundException _e
(git/git-clone url :dir path
:remote "origin"
:branch (or tag "master")
:bare? false
:clone-all? false)))
:remote "origin"
:branch (or tag "master")
:bare? false
:clone-all? false)))
(when callback (callback path))))
(catch org.eclipse.jgit.api.errors.TransportException e
(println (.getMessage e)
Expand Down
Loading