diff --git a/.lsp/sqlite.db b/.lsp/sqlite.db new file mode 100644 index 0000000..97bbd3d Binary files /dev/null and b/.lsp/sqlite.db differ diff --git a/README.md b/README.md index 4365e3a..e581931 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,13 @@ A Leiningen template for FIXME. ## Usage - +### Base only `lein new xiana DEFPROJECT-NAME --to-dir ../CUSTOM-DIR` +### With [nubank/workspaces](https://github.com/nubank/workspaces) : +`+workspaces` Example: +`lein new xiana xi-example +workspaces` `lein new xiana xi-example --to-dir ../lein-xiana-example` diff --git a/resources/leiningen/new/xiana/dev/cljs/user.cljs b/resources/leiningen/new/xiana/dev/cljs/user.cljs index 3f4368d..64416ff 100644 --- a/resources/leiningen/new/xiana/dev/cljs/user.cljs +++ b/resources/leiningen/new/xiana/dev/cljs/user.cljs @@ -3,6 +3,6 @@ development." (:require [cljs.repl :refer (Error->map apropos dir doc error->str ex-str ex-triage - find-doc print-doc pst source)] + find-doc print-doc pst source)] [clojure.pprint :refer (pprint)] [clojure.string :as str])) diff --git a/resources/leiningen/new/xiana/gitignore b/resources/leiningen/new/xiana/gitignore index 87c940f..5e509b1 100644 --- a/resources/leiningen/new/xiana/gitignore +++ b/resources/leiningen/new/xiana/gitignore @@ -12,3 +12,4 @@ pom.xml.asc .hgignore .hg/ .idea/ +.lsp/ diff --git a/resources/leiningen/new/xiana/package.json b/resources/leiningen/new/xiana/package.json new file mode 100644 index 0000000..138037f --- /dev/null +++ b/resources/leiningen/new/xiana/package.json @@ -0,0 +1,33 @@ +{ + "scripts": { + "repl": "shadow-cljs node-repl", + "clean": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf .shadow-cljs/ && rm -rf resources/public/js && rm -rf .lsp/", + "tests": "shadow-cljs compile ci && npx karma start --single-run", + "build": "shadow-cljs release app", + "serve": "shadow-cljs watch app", + "prebuild": "rm ./resources/public/main.css ; postcss ./src/frontend/base.css -o ./resources/public/main.css --env production", + "preserve": "rm ./rsources/public/main.css ; postcss ./src/frontend/base.css -o ./resources/public/main.css" + }, + "dependencies": { + "highlight.js": "9.18.5", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-grid-layout": "1.2.4", + "react-icons": "4.2.0", + "reakit": "1.3.6" + }, + "devDependencies": { + "@fullhuman/postcss-purgecss": "^4.0.2", + "autoprefixer": "^10.2.4", + "cssnano": "^4.1.10", + "postcss": "^8.2.8", + "postcss-cli": "^8.3.1", + "postcss-discard-comments": "^4.0.2", + "shadow-cljs": "^2.11.26", + "source-map-support": "^0.4.18", + "tailwindcss": "^2.0.3", + "@sinonjs/fake-timers": "^7.0.2", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.0.15" + } +} diff --git a/resources/leiningen/new/xiana/postcss.config.js b/resources/leiningen/new/xiana/postcss.config.js new file mode 100644 index 0000000..b24dd52 --- /dev/null +++ b/resources/leiningen/new/xiana/postcss.config.js @@ -0,0 +1,24 @@ +const tailwindcss = require('tailwindcss'); +const discard = require('postcss-discard-comments'); +const cssnano = require('cssnano')({ + preset: 'default', +}) +const purgecss = require('@fullhuman/postcss-purgecss')({ + content: [ + './src/frontend/**/*.css', + './src/frontend/**/*.cljs', + './resources/public/**/*.css', + ], + + defaultExtractor: (content) => content.match(/[\w-/:]+(? + + + + + + + +
+ + + + + diff --git a/resources/leiningen/new/xiana/shadow-cljs.edn b/resources/leiningen/new/xiana/shadow-cljs.edn new file mode 100644 index 0000000..d60b12c --- /dev/null +++ b/resources/leiningen/new/xiana/shadow-cljs.edn @@ -0,0 +1,17 @@ +{:lein true + :nrepl {:port 8777} + :builds {:app + {:target :browser + :build-hooks [(hooks/purge-css!) + (hooks/npm-init!)] + :output-dir "resources/public/js/app" + :asset-path "/js/app" + :modules {:main {:init-fn {{sanitized-name}} .core/init + :preloads [devtools.preload]}}} + {{#workspaces?}} + :workspaces + {:target :browser + :output-dir "resources/public/js/workspaces" + :asset-path "/js/workspaces" + :modules {:main {:entries [{{sanitized-name}}.workspaces] + :preloads [devtools.preload]}}} {{/workspaces?}}}} diff --git a/resources/leiningen/new/xiana/src/backend/app/controllers/index.clj b/resources/leiningen/new/xiana/src/backend/app/controllers/index.clj index ed0b7b5..185369d 100644 --- a/resources/leiningen/new/xiana/src/backend/app/controllers/index.clj +++ b/resources/leiningen/new/xiana/src/backend/app/controllers/index.clj @@ -1,10 +1,12 @@ (ns controllers.index - (:require [xiana.core :as xiana] - [ring.util.response :as ring])) + (:require + [ring.util.response :as ring] + [xiana.core :as xiana])) + (defn handle-index [state] (xiana/ok (assoc state - :response - (ring/response "Index page")))) + :response + (ring/response "Index page")))) diff --git a/resources/leiningen/new/xiana/src/backend/app/controllers/re_frame.clj b/resources/leiningen/new/xiana/src/backend/app/controllers/re_frame.clj index fcbe601..c32b1d6 100644 --- a/resources/leiningen/new/xiana/src/backend/app/controllers/re_frame.clj +++ b/resources/leiningen/new/xiana/src/backend/app/controllers/re_frame.clj @@ -1,12 +1,21 @@ (ns controllers.re-frame (:require [xiana.core :as xiana] [ring.util.response :as ring])) - -(defn handle-index +{{#workspaces?}} +(defn handle-workspaces [state] (xiana/ok (assoc state :response - (-> "index.html" + (-> "workspaces.html" (ring/resource-response {:root "public"}) - (ring/header "Content-Type" "text/html; charset=utf-8"))))) + (ring/header "Content-Type" "text/html; charset=utf-8"))))){{/workspaces?}} + +(defn handle-index + [state] + (xiana/ok + (assoc state + :response + (-> "index.html" + (ring/resource-response {:root "public"}) + (ring/header "Content-Type" "text/html; charset=utf-8"))))) diff --git a/resources/leiningen/new/xiana/src/backend/app/interceptors.clj b/resources/leiningen/new/xiana/src/backend/app/interceptors.clj index 6068a45..15d598e 100644 --- a/resources/leiningen/new/xiana/src/backend/app/interceptors.clj +++ b/resources/leiningen/new/xiana/src/backend/app/interceptors.clj @@ -3,12 +3,15 @@ [potemkin :refer [import-vars]] [xiana.core :as xiana])) + (comment (import-vars [framework.components.app.interceptors sample-router-interceptor sample-controller-interceptor])) -(def sample-{{sanitized-name}}-controller-interceptor + +(def sample- + {{sanitized-name}} -controller-interceptor {:enter (fn [{request :request {:keys [handler controller match]} :request-data :as state}] - (xiana/ok state))}) \ No newline at end of file + (xiana/ok state))}) diff --git a/resources/leiningen/new/xiana/src/backend/app_name.clj b/resources/leiningen/new/xiana/src/backend/app_name.clj index 969160f..646c119 100644 --- a/resources/leiningen/new/xiana/src/backend/app_name.clj +++ b/resources/leiningen/new/xiana/src/backend/app_name.clj @@ -14,6 +14,7 @@ (def routes [["/" {:controller index/handle-index}] ["/re-frame" {:controller re-frame/handle-index}] + {{#workspaces?}}["/workspaces" {:controller re-frame/handle-workspaces}]{{/workspaces?}} ["/assets/*" (ring/create-resource-handler)]]) (defn system @@ -39,3 +40,16 @@ [& _args] (let [config (config/edn)] (component/start (system config)))) + +(defonce st + (-> (config/edn) + system + atom)) + +(defn- start-dev-system + [] + (swap! st component/start)) + +(defn- stop-dev-system + [] + (swap! st component/stop)) diff --git a/resources/leiningen/new/xiana/src/frontend/app_name/views.cljs b/resources/leiningen/new/xiana/src/frontend/app_name/views.cljs index a881fad..9fa4100 100644 --- a/resources/leiningen/new/xiana/src/frontend/app_name/views.cljs +++ b/resources/leiningen/new/xiana/src/frontend/app_name/views.cljs @@ -7,5 +7,6 @@ (defn main-panel [] (let [name (re-frame/subscribe [::subs/name])] [:div - [:h1 "Hello from " @name] + [:h1 {:data-testid "hello"} + "Hello from " @name] ])) diff --git a/resources/leiningen/new/xiana/src/frontend/app_name/workspaces.cljs b/resources/leiningen/new/xiana/src/frontend/app_name/workspaces.cljs new file mode 100644 index 0000000..8f9edc7 --- /dev/null +++ b/resources/leiningen/new/xiana/src/frontend/app_name/workspaces.cljs @@ -0,0 +1,18 @@ +(ns {{sanitized-name}}.workspaces + (:require + [{{sanitized-name}}.events :as events] + [{{sanitized-name}}.views :as views] + [nubank.workspaces.card-types.react :refer [react-card]] + [nubank.workspaces.core :as ws] + [nubank.workspaces.model :as wsm] + [re-frame.core :as re-frame] + [reagent.core :as r])) + +(defonce init (ws/mount)) + +(re-frame/clear-subscription-cache!) + +(ws/defcard init-card + (re-frame/dispatch-sync [::events/initialize-db]) + (react-card + (r/as-element [views/main-panel]))) diff --git a/resources/leiningen/new/xiana/src/frontend/base.css b/resources/leiningen/new/xiana/src/frontend/base.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/resources/leiningen/new/xiana/src/frontend/base.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/resources/leiningen/new/xiana/src/frontend/hooks.clj b/resources/leiningen/new/xiana/src/frontend/hooks.clj new file mode 100644 index 0000000..fb52c98 --- /dev/null +++ b/resources/leiningen/new/xiana/src/frontend/hooks.clj @@ -0,0 +1,44 @@ +(ns hooks + (:require + [clojure.java.io :as io] + [clojure.java.shell :refer [sh]] + [clojure.string :as s])) + + +(defn exec + [& cmd] + (let [cmd (s/split (s/join " " (flatten cmd)) #"\s+") + _ (println (s/join " " cmd)) + {:keys [exit out err]} (apply sh cmd)] + (if (zero? exit) + (when-not (s/blank? out) + (println out)) + (println err)))) + + +(defn npm-init! + {:shadow.build/stage :configure} + [build-state] + (if-not (and (.isDirectory (io/file "node_modules")) + (not (= 0 (count (.list (io/file "node_modules")))))) + (do (println ";;=>> 'Installing node_modules...") + (exec "npm install") + build-state) + (do (println ";;=>> 'npm is already initialized in the current project'") + build-state))) + + +(defn purge-css! + {:shadow.build/stage :flush} + [build-state] + (case (:shadow.build/mode build-state) + :release + (do + (println ";;=> 'Running prebuild script with -env production'") + (exec "npm run-script prebuild") + build-state) + :dev + (do + (println ";;=> 'Runnnig preserve script'") + (exec "npm run-script preserve") + build-state))) diff --git a/resources/leiningen/new/xiana/tailwind.config.js b/resources/leiningen/new/xiana/tailwind.config.js new file mode 100644 index 0000000..40ac096 --- /dev/null +++ b/resources/leiningen/new/xiana/tailwind.config.js @@ -0,0 +1,13 @@ +const colors = require('tailwindcss/colors') + +module.exports ={ + darkMode: 'class', + theme: {}, + variants: {}, + plugins: [], + purge: {enabled: false}, + future: { + removeDeprecatedGapUtilities: true, + prugeLayersByDefault: true, + }, +}; diff --git a/resources/leiningen/new/xiana/test/frontend/app_name/views_test.clj b/resources/leiningen/new/xiana/test/frontend/app_name/views_test.clj new file mode 100644 index 0000000..6822667 --- /dev/null +++ b/resources/leiningen/new/xiana/test/frontend/app_name/views_test.clj @@ -0,0 +1,25 @@ +(ns frontend.{{sanitized-name}}.views-test + (:require [cljs.test :as t + :include-macros true + :refer [are is testing deftest]] + [re-frame.core :as re-frame] + [{{sanitized-name}}.db :as db] + [{{sanitized-name}}.views :as views] + [matcher-combinators.test :refer [match?]] + [frankie.views :as views] + [frankie.db :as db] + [reagent.core :as r] + [frontend.test-utils :as u] + [re-frame.core :as rf] + [day8.re-frame.test :as rf-test])) + +(rf/clear-subscription-cache!) + +(deftest example-counter-test + (rf-test/run-test-sync + (let [rendered-div # (.getByTestId % "hello")] + (u/rf-with-mounted-component + db/default-db + [views/main-panel] + (fn [comp] + (is (= "Hello from re-frame" (rendered-div comp)))))))) diff --git a/resources/leiningen/new/xiana/test/frontend/test_utils.cljs b/resources/leiningen/new/xiana/test/frontend/test_utils.cljs new file mode 100644 index 0000000..28b8c7e --- /dev/null +++ b/resources/leiningen/new/xiana/test/frontend/test_utils.cljs @@ -0,0 +1,107 @@ +(ns frontend.test-utils + (:require + ["@sinonjs/fake-timers" :as timer] + ["@testing-library/react" :as rtl] + ["@testing-library/user-event" :as rtue] + [goog.dom :as gdom] + [re-frame.core :as rf] + [reagent.core :as r])) + + +(def test-container-id "tests-container") + + +(defn create-tests-container! + [] + (let [container (gdom/createDom "div" #js {:id test-container-id})] + (gdom/appendChild (-> js/document .-body) container) + container)) + + +(defn with-mounted-component + [comp f] + (let [container (create-tests-container!) + mounted-component (rtl/render (r/as-element comp) + #js {"container" container})] + (try + (f mounted-component) + (finally + (rtl/cleanup))))) + + +(defn rf-with-mounted-component + [initial-db comp f] + (let [container (create-tests-container!) + mounted-component (rtl/render (r/as-element comp) + #js {"container" container})] + (rf/reg-event-db + ::test-db + (fn [_ _] + initial-db)) + (rf/dispatch-sync [::test-db]) + (try + (f mounted-component) + (finally + (rtl/cleanup))))) + + +(defn ->action-map + [v] + (clj->js (if (map? v) + v + {:target {:value v}}))) + + +(defn click-element! + ([el] + (click-element! el {})) + ([el v-or-m] + (let [click-fn! (.. rtue -default -click)] + (click-fn! el (->action-map v-or-m))) + (r/flush))) + + +(defn double-click-element! + ([el] + (let [click-fn! (.. rtue -default -dblClick)] + (click-fn! el {})) + (r/flush))) + + +(defn submit! + [el] + (.submit rtl/fireEvent el) + (r/flush)) + + +(defn click-context-menu! + [el] + (.contextMenu rtl/fireEvent el) + (r/flush)) + + +(defn input-element! + [el v-or-m] + (.input rtl/fireEvent el (->action-map v-or-m)) + (r/flush)) + + +(defn change-element! + [el v-or-m] + (.change rtl/fireEvent el (->action-map v-or-m)) + (r/flush)) + + +(defn install-timer + [] + (.install timer (.-getTime js/Date.))) + + +(defn tick + [t x] + (.tick t x)) + + +(defn uninstall-timer + [t] + (.uninstall t)) diff --git a/src/leiningen/new/base.clj b/src/leiningen/new/base.clj new file mode 100644 index 0000000..2a80ab7 --- /dev/null +++ b/src/leiningen/new/base.clj @@ -0,0 +1,49 @@ +(ns leiningen.new.base + (:require + [leiningen.new.helpers :as helpers])) + + +(def file-paths + ["Docker/db.Dockerfile" + "Docker/init.sql" + "src/backend/app/controllers/index.clj" + "src/backend/app/controllers/re_frame.clj" + "src/backend/app/controller_behaviors/.gitkeep" + "src/backend/app/db_migrations/.gitkeep" + "src/backend/app/interceptors/.gitkeep" + "src/backend/app/models/.gitkeep" + "src/backend/app/views/layouts/.gitkeep" + "src/backend/app/interceptors.clj" + "src/frontend/deps.cljs" + "src/frontend/base.css" + "src/frontend/hooks.clj" + "src/shared/config.clj" + "src/shared/schema.clj" + "resources/public/index.html" + "config/dev/config.edn" + "config/test/config.edn" + "project.clj" + "package.json" + "postcss.config.js" + "tailwind.config.js" + "docker-compose.yml" + "postgres-start.sh" + "README.md"]) + + +(defn files + [data options] + (->> file-paths + (map (fn [path] [path (helpers/render path data)])) + (concat + [["src/backend/{{name-to-path}}.clj" (helpers/render "src/backend/app_name.clj" data)] + ["src/frontend/{{name-to-path}}/config.cljs" (helpers/render "src/frontend/app_name/config.cljs" data)] + ["src/frontend/{{name-to-path}}/core.cljs" (helpers/render "src/frontend/app_name/core.cljs" data)] + ["src/frontend/{{name-to-path}}/db.cljs" (helpers/render "src/frontend/app_name/db.cljs" data)] + ["src/frontend/{{name-to-path}}/events.cljs" (helpers/render "src/frontend/app_name/events.cljs" data)] + ["src/frontend/{{name-to-path}}/subs.cljs" (helpers/render "src/frontend/app_name/subs.cljs" data)] + ["src/frontend/{{name-to-path}}/views.cljs" (helpers/render "src/frontend/app_name/views.cljs" data)] + ["test/{{name-to-path}}_test.clj" (helpers/render "test/app_name_test.clj" data)] + [".gitignore" (helpers/render "gitignore" data)] + ["shadow-cljs.edn" (helpers/render "shadow-cljs.edn" data)] + [".hgignore" (helpers/render "hgignore" data)]]))) diff --git a/src/leiningen/new/helpers.clj b/src/leiningen/new/helpers.clj new file mode 100644 index 0000000..e75a48f --- /dev/null +++ b/src/leiningen/new/helpers.clj @@ -0,0 +1,29 @@ +(ns leiningen.new.helpers + (:require + [clojure.java.io :as io] + [leiningen.new.templates :refer [renderer sanitize name-to-path]])) + + +(def template-name "xiana") + +(def render-text (renderer template-name)) + + +(defn resource-input + [resource-path] + (-> (str "leiningen/new/" (sanitize template-name) "/" resource-path) + io/resource + io/input-stream)) + + +(defn render + ([resource-path] + (resource-input resource-path)) + ([resource-path data] + (render-text resource-path data))) + + +(defn options? + [option-name options] + (boolean + (some #{option-name} options))) diff --git a/src/leiningen/new/workspaces.clj b/src/leiningen/new/workspaces.clj new file mode 100644 index 0000000..172490c --- /dev/null +++ b/src/leiningen/new/workspaces.clj @@ -0,0 +1,12 @@ +(ns leiningen.new.workspaces + (:require + [leiningen.new.helpers :as helpers])) + + +(def option "+workspaces") + + +(defn files + [data] + [["src/frontend/{{name-to-path}}/workspaces.cljs" (helpers/render "src/frontend/app_name/workspaces.cljs" data)] + ["resources/public/workspaces.html" (helpers/render "resources/public/workspaces.html" data)]]) diff --git a/src/leiningen/new/xiana.clj b/src/leiningen/new/xiana.clj index b136a29..db1e2da 100644 --- a/src/leiningen/new/xiana.clj +++ b/src/leiningen/new/xiana.clj @@ -1,47 +1,56 @@ (ns leiningen.new.xiana - (:require [leiningen.new.templates :refer [multi-segment sanitize-ns renderer name-to-path ->files]] - [leiningen.core.main :as main])) + (:require + [clojure.set :as clset] + [clojure.string :as clstring] + [leiningen.core.main :as main] + [leiningen.new.base :as base] + [leiningen.new.helpers :as helpers] + [leiningen.new.templates :refer [multi-segment sanitize-ns name-to-path ->files]] + [leiningen.new.workspaces :as workspaces])) + + +(declare template-data check-options app-files) + + +(def available-options + #{workspaces/option}) -(def render (renderer "xiana")) (defn xiana - "FIXME: write documentation" - [name] - (let [data {:name name - :namespace (multi-segment (sanitize-ns name)) - :sanitized-name (sanitize-ns name) - :name-to-path (name-to-path name)}] - (main/info "Generating fresh 'lein new' xiana project.") - (apply ->files data (->> ["Docker/db.Dockerfile" - "Docker/init.sql" - "src/backend/app/controllers/index.clj" - "src/backend/app/controllers/re_frame.clj" - "src/backend/app/controller_behaviors/.gitkeep" - "src/backend/app/db_migrations/.gitkeep" - "src/backend/app/interceptors/.gitkeep" - "src/backend/app/models/.gitkeep" - "src/backend/app/views/layouts/.gitkeep" - "src/backend/app/interceptors.clj" - "src/frontend/deps.cljs" - "src/shared/config.clj" - "src/shared/schema.clj" - "resources/public/index.html" - "config/dev/config.edn" - "config/test/config.edn" - "project.clj" - "docker-compose.yml" - "postgres-start.sh" - "README.md"] - (map (fn [path] [path (render path data)])) - (concat - [["src/backend/{{name-to-path}}.clj" (render "src/backend/app_name.clj" data)] - ["src/frontend/{{name-to-path}}/config.cljs" (render "src/frontend/app_name/config.cljs" data)] - ["src/frontend/{{name-to-path}}/core.cljs" (render "src/frontend/app_name/core.cljs" data)] - ["src/frontend/{{name-to-path}}/db.cljs" (render "src/frontend/app_name/db.cljs" data)] - ["src/frontend/{{name-to-path}}/events.cljs" (render "src/frontend/app_name/events.cljs" data)] - ["src/frontend/{{name-to-path}}/subs.cljs" (render "src/frontend/app_name/subs.cljs" data)] - ["src/frontend/{{name-to-path}}/views.cljs" (render "src/frontend/app_name/views.cljs" data)] - ["test/{{name-to-path}}_test.clj" (render "test/app_name_test.clj" data)] - ["test/{{name-to-path}}_fixture.clj" (render "test/app_name_fixture.clj" data)] - [".gitignore" (render "gitignore" data)] - [".hgignore" (render "hgignore" data)]]))))) + [name & options] + (println options) + (let [data (template-data name options)] + (check-options options) + (main/info "Generation fresh 'lein new' xiana project.") + (apply ->files data (app-files data options)))) + + +(defn check-available + [options] + (let [options-set (into #{} options) + abort? (not (clset/superset? available-options options-set))] + (when abort? + (main/abort "\nError: invalid option(s)\nAvailable: " + (clstring/join " " (sort available-options)) "\n")))) + + +(defn check-options + [options] + (doto options + check-available)) + + +(defn template-data + [name options] + {:name name + :namespace (multi-segment (sanitize-ns name)) + :sanitized-name (sanitize-ns name) + :name-to-path (name-to-path name) + :workspaces? (helpers/options? "+workspaces" options)}) + + +(defn app-files + [data options] + (concat + (when (helpers/options? workspaces/option options) (workspaces/files data)) + (base/files data options)))