├── examples ├── nbb │ ├── .gitignore │ ├── package.json │ ├── README.md │ └── webserver.cljs ├── templates │ ├── .gitignore │ ├── package.json │ ├── README.md │ ├── index.html │ └── webserver.cljs ├── send-email │ ├── .gitignore │ ├── package.json │ ├── README.md │ └── email-example.cljs ├── form-validation │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── index.html │ └── webserver.cljs ├── nbb-live-reloading │ ├── css │ │ ├── style.css │ │ └── minimal-stylesheet │ ├── README.md │ ├── package.json │ ├── index.html │ └── server.cljs ├── nbb-auth │ ├── .gitignore │ ├── package.json │ ├── README.md │ ├── index.html │ └── webserver.cljs ├── README.md ├── authentication │ ├── .clj-kondo │ │ └── config.edn │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── shadow-cljs.edn │ ├── index.html │ └── src │ │ └── authexample │ │ └── server.cljs └── shadow-cljs │ ├── .gitignore │ ├── README.md │ ├── shadow-cljs.edn │ ├── package.json │ └── src │ └── shadowtest │ └── server.cljs ├── create ├── sitefox-nbb │ ├── .gitignore │ ├── template │ │ ├── .clj-kondo │ │ │ └── config.edn │ │ ├── gitignore │ │ ├── deps.edn │ │ ├── public │ │ │ ├── index.html │ │ │ └── style.css │ │ ├── package.json │ │ └── server.cljs │ ├── package.json │ ├── README.md │ └── index.js ├── sitefox-fullstack │ ├── .gitignore │ ├── template │ │ ├── .clj-kondo │ │ │ └── config.edn │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ ├── public │ │ │ ├── index.html │ │ │ └── css │ │ │ │ └── style.css │ │ ├── shadow-cljs.edn │ │ ├── src │ │ │ └── NAME │ │ │ │ ├── ui.cljs │ │ │ │ └── server.cljs │ │ └── Makefile │ ├── package.json │ ├── README.md │ └── index.js └── README.md ├── .clj-kondo └── config.edn ├── .gitignore ├── bin ├── test-on-postgres ├── sync-deps.cljs ├── generate-docs.cljs └── update-readme-versions.cljs ├── shadow-cljs.edn ├── src ├── deps.cljs └── sitefox │ ├── docs.clj │ ├── util.cljs │ ├── logging.cljs │ ├── deps.cljc │ ├── reloader.cljs │ ├── tracebacks.cljs │ ├── mail.cljs │ ├── ui.cljs │ ├── html.cljs │ ├── db.cljs │ └── web.cljs ├── LICENSE ├── deps.edn ├── docs ├── css │ ├── highlight.css │ └── default.css ├── js │ ├── page_effects.js │ └── highlight.min.js ├── .html ├── sitefox.deps.html ├── sitefox.tracebacks.html ├── sitefox.reloader.html ├── sitefox.util.html ├── logo.svg ├── sitefox.logging.html ├── sitefox.mail.html ├── sitefox.db.html ├── sitefox.html.html ├── sitefox.ui.html └── sitefox.web.html ├── package.json └── .github └── workflows └── tests.yml /examples/nbb/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | database.sqlite 3 | -------------------------------------------------------------------------------- /create/sitefox-nbb/.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | node_modules 3 | -------------------------------------------------------------------------------- /examples/templates/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | database.sqlite 3 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | node_modules 3 | -------------------------------------------------------------------------------- /examples/send-email/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | database.sqlite 3 | -------------------------------------------------------------------------------- /examples/form-validation/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | database.sqlite 3 | -------------------------------------------------------------------------------- /examples/nbb-live-reloading/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /examples/nbb-auth/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | database.sqlite 3 | node_modules 4 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples of Sitefox components used in different environments. 2 | -------------------------------------------------------------------------------- /examples/nbb-live-reloading/css/minimal-stylesheet: -------------------------------------------------------------------------------- 1 | ../node_modules/minimal-stylesheet -------------------------------------------------------------------------------- /examples/authentication/.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {promesa.core/let clojure.core/let}} 2 | -------------------------------------------------------------------------------- /create/sitefox-nbb/template/.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {promesa.core/let clojure.core/let}} 2 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {promesa.core/let clojure.core/let}} 2 | -------------------------------------------------------------------------------- /create/sitefox-nbb/template/gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .*.swp 3 | logs 4 | database.sqlite 5 | .clj-kondo/.cache 6 | -------------------------------------------------------------------------------- /examples/shadow-cljs/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .shadow-cljs 3 | database.sqlite 4 | logs 5 | server.js 6 | devserver.js 7 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {promesa.core/let clojure.core/let 2 | applied-science.js-interop/let clojure.core/let}} 3 | -------------------------------------------------------------------------------- /examples/authentication/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .shadow-cljs 3 | database.sqlite 4 | logs 5 | tests.js 6 | server.js 7 | devserver.js 8 | .clj-kondo/.cache 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | node_modules 3 | package-lock.json 4 | workspace 5 | **/.clj-kondo/.cache 6 | tests.js 7 | tests.sqlite 8 | database.sqlite 9 | .cpcache 10 | .shadow-cljs 11 | .lsp 12 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | database.sqlite 3 | public/js 4 | .shadow-cljs 5 | .clj-kondo/.cache 6 | node_modules 7 | logs 8 | database.sqlite 9 | devserver.js 10 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "concurrently": "^8.2.1", 4 | "shadow-cljs": "^2.25.3", 5 | "sitefox": "github:chr15m/sitefox" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /create/sitefox-nbb/template/deps.edn: -------------------------------------------------------------------------------- 1 | {;; this deps.edn enables clojure-lsp features 2 | :paths ["."] 3 | :deps 4 | {io.github.chr15m/sitefox {:git/tag "v0.0.1" :git/sha "b5520678b9fbf5919878a75750bb4d792c9eb8e2"}}} 5 | -------------------------------------------------------------------------------- /examples/send-email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "nbb": "0.0.97", 4 | "sitefox": "file:../../" 5 | }, 6 | "scripts": { 7 | "send": "nbb --classpath ../../src/ email-example.cljs" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/README.md: -------------------------------------------------------------------------------- 1 | A Sitefox + shadow-cljs web app. 2 | 3 | # Dev 4 | 5 | ``` 6 | npm install 7 | make watch 8 | ``` 9 | 10 | # Build 11 | 12 | ``` 13 | make 14 | cd build && node server.js 15 | ``` 16 | -------------------------------------------------------------------------------- /bin/test-on-postgres: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dropdb sitefoxtest 2>/dev/null || echo "Not dropping database." 4 | createdb sitefoxtest && SECRET=testing TESTING=1 DATABASE_URL="postgres://%2Fvar%2Frun%2Fpostgresql/sitefoxtest" npx shadow-cljs compile test 5 | -------------------------------------------------------------------------------- /create/README.md: -------------------------------------------------------------------------------- 1 | Create scripts for `npm init`. 2 | 3 | - `npm init sitefox-fullstack` for a "full stack" project compiled with shadow-cljs on both backend and frontend. 4 | - `npm init sitefox-nbb` for a backend-only project running on [nbb](https://github.com/babashka/nbb). 5 | 6 | -------------------------------------------------------------------------------- /examples/nbb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "filewatcher": "^3.0.1", 4 | "nbb": "^1.2.179", 5 | "react": "^17.0.2", 6 | "react-dom": "^17.0.2", 7 | "sitefox": "file:../../" 8 | }, 9 | "scripts": { 10 | "serve": "DEV=1 nbb --classpath ../../src/ webserver.cljs" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/nbb-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "filewatcher": "^3.0.1", 4 | "nbb": "^1.2.179", 5 | "react": "^17.0.2", 6 | "react-dom": "^17.0.2", 7 | "sitefox": "file:../../" 8 | }, 9 | "scripts": { 10 | "serve": "DEV=1 nbb --classpath ../../src/ webserver.cljs" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/nbb/README.md: -------------------------------------------------------------------------------- 1 | Create a sitefox server using [nbb](https://github.com/borkdude/nbb). 2 | 3 | To test this out: 4 | 5 | 1. Clone this repo. 6 | 2. Go into `examples/nbb`. 7 | 3. `npm i --no-save` 8 | 4. `npm run serve` 9 | 10 | This will run `webserver.clj` using nbb, and then you can connect at https://localhost:8000/. 11 | -------------------------------------------------------------------------------- /examples/templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "filewatcher": "^3.0.1", 4 | "minimal-stylesheet": "^0.1.0", 5 | "nbb": "0.0.97", 6 | "react": "^17.0.2", 7 | "react-dom": "^17.0.2", 8 | "sitefox": "file:../../" 9 | }, 10 | "scripts": { 11 | "serve": "DEV=1 nbb --classpath ../../src/ webserver.cljs" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/nbb-auth/README.md: -------------------------------------------------------------------------------- 1 | This example demonstrates Sitefox authentication in an [nbb](https://github.com/borkdude/nbb) webserver. 2 | 3 | To test this out: 4 | 5 | 1. Clone this repo. 6 | 2. Go into `examples/nbb-auth`. 7 | 3. `npm i --no-save` 8 | 4. `npm run serve` 9 | 10 | This will run `webserver.clj` using nbb, and then you can connect at https://localhost:8000/. 11 | -------------------------------------------------------------------------------- /examples/form-validation/README.md: -------------------------------------------------------------------------------- 1 | Sitefox form validation & CSRF prevention example using [nbb](https://github.com/borkdude/nbb). 2 | 3 | To test this out: 4 | 5 | 1. Clone this repo. 6 | 2. Go into `examples/form-validation`. 7 | 3. `npm i --no-save` 8 | 4. `npm run serve` 9 | 10 | This will run `webserver.clj` using nbb, and then you can connect at https://localhost:8000/. 11 | -------------------------------------------------------------------------------- /examples/templates/README.md: -------------------------------------------------------------------------------- 1 | Rendering Reagent into an existing HTML document on the server using [nbb](https://github.com/borkdude/nbb). 2 | 3 | To test this out: 4 | 5 | 1. Clone this repo. 6 | 2. Go into `examples/templates`. 7 | 3. `npm i --no-save` 8 | 4. `npm run serve` 9 | 10 | This will run `webserver.clj` using nbb, and then you can connect at https://localhost:8000/. 11 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dependencies [[reagent "1.1.0"] 3 | [funcool/promesa "8.0.450"] 4 | [applied-science/js-interop "0.4.2"] 5 | [codox "0.10.8"]] 6 | :builds {:test {:target :node-test 7 | :output-to "tests.js" 8 | :ns-regexp "sitefox\\..*$" 9 | :autorun true}}} 10 | -------------------------------------------------------------------------------- /create/sitefox-nbb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-sitefox-nbb", 3 | "description": "Set up a ClojureScript web server in one command with sitefox and nbb.", 4 | "repository": "https://github.com/chr15m/sitefox", 5 | "version": "0.0.9", 6 | "bin": { 7 | "create-sitefox-nbb": "index.js" 8 | }, 9 | "dependencies": { 10 | "fs-extra": "^9.0.1", 11 | "replace-in-file": "^6.1.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-sitefox-fullstack", 3 | "description": "Set up a ClojureScript web server in one command with sitefox and shadow-cljs.", 4 | "repository": "https://github.com/chr15m/sitefox", 5 | "version": "0.0.8", 6 | "bin": { 7 | "create-sitefox-fullstack": "index.js" 8 | }, 9 | "dependencies": { 10 | "fs-extra": "^9.0.1", 11 | "replace-in-file": "^6.1.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /create/sitefox-nbb/template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sitefox HTML template merge example 6 | 7 | 8 | 9 | 10 |
11 |

12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/send-email/README.md: -------------------------------------------------------------------------------- 1 | Send emails with Sitefox using [nbb](https://github.com/borkdude/nbb). 2 | 3 | To test this out: 4 | 5 | 1. Clone this repo. 6 | 2. Go into `examples/send-email`. 7 | 3. `npm i` 8 | 4. `npm run send` 9 | 10 | This will run `email-example.clj` using nbb, and you will see the test email results output. 11 | 12 | This demo uses ethereal.email unless `SMTP_URL` is configured. 13 | Results will be printed to the console and not sent. 14 | -------------------------------------------------------------------------------- /examples/form-validation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "filewatcher": "^3.0.1", 4 | "minimal-stylesheet": "^0.1.0", 5 | "nbb": "^1.2.180", 6 | "node-input-validator": "^4.4.1", 7 | "react": "^17.0.2", 8 | "react-dom": "^17.0.2", 9 | "sitefox": "file:../../" 10 | }, 11 | "scripts": { 12 | "serve": "DEV=1 nbb --classpath ../../src/ webserver.cljs", 13 | "serve-live": "nbb --classpath ../../src/ webserver.cljs" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/authentication/README.md: -------------------------------------------------------------------------------- 1 | Sitefox email & password authentication example using [shadow-cljs](https://shadow-cljs.github.io/docs/UsersGuide.html). 2 | 3 | To run this example: 4 | 5 | 1. Clone this repo. 6 | 2. Go into `examples/authentication`. 7 | 3. `npm i --no-save` 8 | 4. `npm run serve` 9 | 10 | This will compile `src/shadowtest/server.cljs` to `server.js` and then run it. You can connect at http://localhost:8000/. 11 | 12 | To run the live-reloading server in dev mode use `npm run dev` instead. 13 | -------------------------------------------------------------------------------- /bin/sync-deps.cljs: -------------------------------------------------------------------------------- 1 | (ns update-deps 2 | (:require 3 | ["fs" :as fs] 4 | [clojure.edn :as edn] 5 | [clojure.pprint :refer [pprint]])) 6 | 7 | (let [package (js/require "../package.json") 8 | js-deps (js->clj (aget package "dependencies")) 9 | deps (edn/read-string (fs/readFileSync "src/deps.cljs" "utf8")) 10 | deps-updated (assoc deps :npm-deps js-deps)] 11 | (binding [*print-fn* (fn [s] 12 | (fs/writeFileSync "src/deps.cljs" s))] 13 | (pprint deps-updated))) 14 | -------------------------------------------------------------------------------- /examples/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sitefox HTML template merge example 6 | 7 | 8 | 9 | 10 | 11 |
12 |

13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/shadow-cljs/README.md: -------------------------------------------------------------------------------- 1 | Create a sitefox server using [shadow-cljs](https://shadow-cljs.github.io/docs/UsersGuide.html). 2 | 3 | To run this example: 4 | 5 | 1. Clone this repo. 6 | 2. Go into `examples/shadow-cljs`. 7 | 3. `npm i --no-save` 8 | 4. `npm run serve` 9 | 10 | This will start the live-reloading shadow-cljs server compiling devserver.js and run it. 11 | Once you see the message that the server is up you can connect at http://localhost:8000/. 12 | 13 | You can run `npm run build` to get a compiled `server.js` for deployment to live systems. 14 | -------------------------------------------------------------------------------- /examples/form-validation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sitefox HTML form validation example 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/nbb-live-reloading/README.md: -------------------------------------------------------------------------------- 1 | Create a sitefox server using [nbb](https://github.com/borkdude/nbb) with live-reloading via [browser-sync](https://github.com/BrowserSync/browser-sync). 2 | 3 | To test this out: 4 | 5 | 1. Clone this repo. 6 | 2. Go into `examples/nbb`. 7 | 3. `npm i --no-save` 8 | 4. `npm run serve` 9 | 10 | This will run `server.cljs` using nbb. Browser sync will automatically open a tab with your app loaded. If you edit `server.cljs` the page will be refreshed. If you edit `css/style.css` the styles will be hot-loaded without refreshing the page. 11 | -------------------------------------------------------------------------------- /examples/nbb-live-reloading/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nbb-synctest", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "browser-sync": "^2.27.7", 6 | "minimal-stylesheet": "0.1.0", 7 | "nbb": "0.0.107", 8 | "react": "^17.0.2", 9 | "react-dom": "^17.0.2", 10 | "sitefox": "chr15m/sitefox" 11 | }, 12 | "devDependencies": { 13 | "filewatcher": "^3.0.1" 14 | }, 15 | "scripts": { 16 | "serve": "DEV=1 nbb --classpath node_modules/sitefox/src/ server.cljs", 17 | "serve-live": "nbb --classpath node_modules/sitefox/src/ server.cljs" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NAME 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-deps 2 | {"passport-local" "1.0.0", 3 | "html-to-text" "8.1.0", 4 | "rotating-file-stream" "3.0.4", 5 | "csrf-csrf" "3.0.3", 6 | "node-html-parser" "4.1.5", 7 | "json-stringify-safe" "5.0.1", 8 | "nodemailer" "6.9.9", 9 | "cookie-parser" "1.4.5", 10 | "source-map-support" "0.5.21", 11 | "react-dom" "17.0.2", 12 | "morgan" "1.10.0", 13 | "passport" "0.6.0", 14 | "node-input-validator" "4.5.0", 15 | "express" "4.21.0", 16 | "tmp" "0.2.1", 17 | "express-session" "1.17.2", 18 | "react" "17.0.2", 19 | "keyv" "4.5.2", 20 | "@keyv/sqlite" "3.6.5"}} 21 | -------------------------------------------------------------------------------- /examples/shadow-cljs/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src" "../../src"] 2 | :dependencies [[reagent "1.1.0"] 3 | [funcool/promesa "6.0.2"] 4 | [applied-science/js-interop "0.2.7"]] 5 | :builds {:server {:target :node-script 6 | :output-to "devserver.js" 7 | :main shadowtest.server/main! 8 | :modules {:server {:init-fn shadowtest.server/main!}} 9 | :release {:output-to "server.js" 10 | :output-dir "build"} 11 | :devtools {:after-load shadowtest.server/reload!}}}} 12 | -------------------------------------------------------------------------------- /examples/shadow-cljs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "concurrently": "^6.3.0", 4 | "react": "^17.0.2", 5 | "react-dom": "^17.0.2", 6 | "shadow-cljs": "^2.20.20" 7 | }, 8 | "scripts": { 9 | "devserver": "rm -f devserver.js; until [ -f devserver.js ]; do sleep 1; done; sleep 1 && while [ 1 ]; do node devserver.js; sleep 3; done;", 10 | "watch": "shadow-cljs watch server", 11 | "serve": "concurrently --kill-others \"npm run watch\" \"npm run devserver\"", 12 | "build": "shadow-cljs release server", 13 | "serve-live": "shadow-cljs release server && node server.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/nbb-live-reloading/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sitefox HTML template merge example 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/sitefox/docs.clj: -------------------------------------------------------------------------------- 1 | (ns sitefox.docs 2 | (:require [codox.main :refer [generate-docs]])) 3 | 4 | (generate-docs 5 | {:source-paths ["src"] 6 | :name "Sitefox" 7 | ;:namespaces [sitefox.web] 8 | ;:namespaces [sitefox.ui] 9 | ;:namespaces [sitefox.hello] 10 | ;:namespaces [sitefox.web sitefox.db sitefox.html sitefox.mail sitefox.logging sitefox.util sitefox.ui sitefox.reloader] 11 | :source-uri "https://github.com/chr15m/sitefox/blob/master/{filepath}#L{line}" 12 | :metadata {:doc/format :markdown} 13 | :output-path "docs" 14 | :reader :clojurescript 15 | :language :clojurescript 16 | :doc-files ["docs/README.md"]}) 17 | -------------------------------------------------------------------------------- /create/sitefox-nbb/README.md: -------------------------------------------------------------------------------- 1 | Set up a ClojureScript web server in one command using 2 | [sitefox](https://github.com/chr15m/sitefox) 3 | and [nbb](https://github.com/borkdude/nbb). 4 | 5 | ```shell 6 | npm create sitefox-nbb mywebsite 7 | cd mywebsite 8 | npm run serve 9 | ``` 10 | 11 | Then open `server.cljs` in your editor and start hacking. 12 | When `server.cljs` is modified the server will automatically reload routes. 👍 13 | 14 | To serve the live version without file watcher reloading: 15 | 16 | ``` 17 | npm run serve-live 18 | ``` 19 | 20 | See the [sitefox documentation](https://github.com/chr15m/sitefox#batteries-included) for details on what you can do next. 21 | -------------------------------------------------------------------------------- /bin/generate-docs.cljs: -------------------------------------------------------------------------------- 1 | (ns generate-docs 2 | (:require 3 | ["fs" :as fs] 4 | ["child_process" :refer [execSync]])) 5 | 6 | (let [readme (-> (fs/readFileSync "README.md") 7 | .toString 8 | (.replace "docs/" ""))] 9 | (fs/writeFileSync "docs/README.md" (str "# Readme\n\n" readme)) 10 | (fs/renameSync "src/sitefox/deps.cljc" "x") 11 | (fs/writeFileSync "src/sitefox/deps.cljc" "(ns sitefox.deps)\n") 12 | ; TODO: print stderr 13 | ;(execSync "clojure -X:codox") 14 | (execSync "npx shadow-cljs run sitefox.docs/generate-docs") 15 | (fs/rmSync "docs/README.md") 16 | (fs/rmSync "src/sitefox/deps.cljc") 17 | (fs/renameSync "x" "src/sitefox/deps.cljc")) 18 | -------------------------------------------------------------------------------- /create/sitefox-nbb/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NAME", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "nbb": "1.3.196", 6 | "react": "^17.0.2", 7 | "react-dom": "^17.0.2", 8 | "sitefox": "chr15m/sitefox" 9 | }, 10 | "devDependencies": { 11 | "browser-sync": "^2.27.7", 12 | "fast-glob": "^3.2.11", 13 | "filewatcher": "^3.0.1" 14 | }, 15 | "scripts": { 16 | "serve": "DEV=1 nbb --classpath ${VIRTUAL_ENV:=.}/node_modules/sitefox/src/ server.cljs", 17 | "serve-live": "nbb --classpath ${VIRTUAL_ENV:=.}/node_modules/sitefox/src/ server.cljs", 18 | "repl": "DEV=1 nbb --classpath ${VIRTUAL_ENV:=.}/node_modules/sitefox/src/ nrepl-server :port 1338" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /create/sitefox-nbb/template/public/style.css: -------------------------------------------------------------------------------- 1 | html, body, #app, #loading { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | max-width: 100%; 7 | width: 800px; 8 | font-family: Helvetica, Arial, sans-serif; 9 | margin: auto; 10 | } 11 | 12 | #loading { 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | } 17 | 18 | #loading div { 19 | animation: spin 0.33s linear infinite; 20 | width: 48px; 21 | height: 48px; 22 | border-radius: 24px; 23 | border: 3px solid transparent; 24 | border-left: 3px solid silver; 25 | border-right: 3px solid silver; 26 | } 27 | 28 | @keyframes spin { 29 | 0% { transform: rotate(0deg); } 30 | 100% { transform: rotate(360deg); } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/public/css/style.css: -------------------------------------------------------------------------------- 1 | html, body, #app, #loading { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | max-width: 100%; 7 | width: 800px; 8 | font-family: Helvetica, Arial, sans-serif; 9 | margin: auto; 10 | } 11 | 12 | #loading { 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | } 17 | 18 | #loading div { 19 | animation: spin 0.33s linear infinite; 20 | width: 48px; 21 | height: 48px; 22 | border-radius: 24px; 23 | border: 3px solid transparent; 24 | border-left: 3px solid silver; 25 | border-right: 3px solid silver; 26 | } 27 | 28 | @keyframes spin { 29 | 0% { transform: rotate(0deg); } 30 | 100% { transform: rotate(360deg); } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /examples/authentication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "concurrently": "^6.3.0", 4 | "minimal-stylesheet": "^0.1.0", 5 | "passport": "^0.6.0", 6 | "passport-local": "^1.0.0", 7 | "react": "^17.0.2", 8 | "react-dom": "^17.0.2", 9 | "shadow-cljs": "^2.18.0" 10 | }, 11 | "scripts": { 12 | "devserver": "rm -f devserver.js; until [ -f devserver.js ]; do sleep 1; done; sleep 1 && while [ 1 ]; do node devserver.js; sleep 3; done;", 13 | "watch": "shadow-cljs watch server", 14 | "serve": "concurrently --kill-others \"npm run watch\" \"npm run devserver\"", 15 | "build": "shadow-cljs release server", 16 | "serve-live": "shadow-cljs release server && node server.js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src" "node_modules/sitefox/src"] 2 | :dependencies [[reagent "1.0.0-alpha2"] 3 | [applied-science/js-interop "0.2.7"] 4 | [funcool/promesa "6.0.2"]] 5 | :builds {:server {:target :node-script 6 | :output-to "devserver.js" 7 | :main NAME.server/main! 8 | :release {:output-to "build/server.js"}} 9 | :app {:target :browser 10 | :output-dir "public/js" 11 | :asset-path "/js" 12 | :modules {:main {:init-fn NAME.ui/main!}} 13 | :devtools {:watch-dir "public"} 14 | :release {:output-dir "build/public/js"}}}} 15 | 16 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/src/NAME/ui.cljs: -------------------------------------------------------------------------------- 1 | (ns NAME.ui 2 | (:require [reagent.core :as r] 3 | [reagent.dom :as rdom])) 4 | 5 | (defonce state (r/atom {})) 6 | 7 | (defn button-clicked [_ev] 8 | (swap! state update-in [:number] inc)) 9 | 10 | (defn component-main [state] 11 | [:div 12 | [:h1 "NAME"] 13 | [:p "Welcome to the app!"] 14 | [:button {:on-click button-clicked} "click me"] 15 | [:pre (pr-str @state)] 16 | [:p [:a {:href "/mypage"} "Static server rendered page."]] 17 | [:p [:a {:href "/api/example.json"} "JSON API example."]]]) 18 | 19 | (defn start {:dev/after-load true} [] 20 | (rdom/render [component-main state] 21 | (js/document.getElementById "app"))) 22 | 23 | (defn main! [] 24 | (start)) 25 | -------------------------------------------------------------------------------- /examples/nbb/webserver.cljs: -------------------------------------------------------------------------------- 1 | (ns webserver 2 | (:require 3 | [promesa.core :as p] 4 | [reagent.dom.server :refer [render-to-static-markup] :rename {render-to-static-markup r}] 5 | [nbb.core :refer [*file*]] 6 | [sitefox.reloader :refer [nbb-reloader]] 7 | [sitefox.web :as web])) 8 | 9 | (defn root-view [_req res] 10 | (.send res (r [:h1 "Hello world!"]))) 11 | 12 | (defn setup-routes [app] 13 | (web/reset-routes app) 14 | (.get app "/" root-view)) 15 | 16 | (defonce init 17 | (p/catch 18 | (p/let [self *file* 19 | [app host port] (web/start)] 20 | (setup-routes app) 21 | (nbb-reloader self #(setup-routes app)) 22 | (println "Serving on" (str "http://" host ":" port))) 23 | (fn [err] (js/console.error err)))) 24 | -------------------------------------------------------------------------------- /examples/authentication/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src" "../../src"] 2 | :dependencies [[reagent "1.1.0"] 3 | [funcool/promesa "6.0.2"] 4 | [applied-science/js-interop "0.2.7"]] 5 | :builds {:server {:target :node-script 6 | :output-to "devserver.js" 7 | :main authexample.server/main! 8 | :modules {:server {:init-fn authexample.server/main!}} 9 | :release {:output-to "server.js" 10 | :output-dir "build"} 11 | :devtools {:after-load authexample.server/reload!}} 12 | :test {:target :node-test 13 | :output-to "tests.js" 14 | :ns-regexp "sitefox.*$" 15 | :autorun true}}} 16 | -------------------------------------------------------------------------------- /examples/shadow-cljs/src/shadowtest/server.cljs: -------------------------------------------------------------------------------- 1 | (ns shadowtest.server 2 | (:require 3 | [promesa.core :as p] 4 | [sitefox.html :refer [render]] 5 | [sitefox.web :as web] 6 | [sitefox.logging :refer [bind-console-to-file]])) 7 | 8 | (bind-console-to-file) 9 | 10 | (defonce server (atom nil)) 11 | 12 | (defn home-page [_req res] 13 | (.send res (render [:h1 "Hello world! yes"]))) 14 | 15 | (defn setup-routes [app] 16 | (web/reset-routes app) 17 | (.get app "/" home-page)) 18 | 19 | (defn main! [] 20 | (p/let [[app host port] (web/start)] 21 | (reset! server app) 22 | (setup-routes app) 23 | (println "Server listening on" (str "http://" host ":" port)))) 24 | 25 | (defn ^:dev/after-load reload [] 26 | (js/console.log "reload") 27 | (setup-routes @server)) 28 | -------------------------------------------------------------------------------- /bin/update-readme-versions.cljs: -------------------------------------------------------------------------------- 1 | (ns update-readme 2 | (:require 3 | ["fs" :as fs] 4 | ["child_process" :refer [execSync]])) 5 | 6 | (defn replace-version [tag sha line] 7 | (let [updated-line (str " {io.github.chr15m/sitefox {:git/tag \"" tag "\" :git/sha \"" sha "\"}}}")] 8 | (if (> (.indexOf line "git/sha") -1) 9 | updated-line 10 | line))) 11 | 12 | (let [package (-> (fs/readFileSync "package.json") js/JSON.parse) 13 | readme (-> (fs/readFileSync "README.md") .toString) 14 | lines (.split readme "\n") 15 | sha (-> (execSync "git rev-parse HEAD") .toString .trim) 16 | version (aget package "version") 17 | tag (str "v" version) 18 | updated (.map lines (partial replace-version tag sha)) 19 | readme-new (.join updated "\n")] 20 | (fs/writeFileSync "README.md" readme-new)) 21 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/README.md: -------------------------------------------------------------------------------- 1 | Set up a full-stack ClojureScript web server in one command using 2 | [sitefox](https://github.com/chr15m/sitefox) 3 | and [shadow-cljs](https://shadow-cljs.github.io/docs/UsersGuide.html). 4 | 5 | ```shell 6 | npm create sitefox-fullstack mywebapp 7 | cd mywebapp 8 | npm install 9 | make watch 10 | ``` 11 | 12 | Then open `src/mywebapp/server.cljs` to edit the back end code. 13 | Open `src/mywebapp/ui.cljs` to edit the front end code. 14 | Code will be automatically reloaded. 👍 15 | 16 | # Production 17 | 18 | To make a production build into the `build` folder: 19 | 20 | ``` 21 | make 22 | ``` 23 | 24 | To run the production build: 25 | 26 | ``` 27 | cd build && node server.js 28 | ``` 29 | 30 | See the [sitefox documentation](https://github.com/chr15m/sitefox#batteries-included) for details on what you can do next. 31 | -------------------------------------------------------------------------------- /examples/templates/webserver.cljs: -------------------------------------------------------------------------------- 1 | (ns webserver 2 | (:require 3 | ["fs" :as fs] 4 | [promesa.core :as p] 5 | [sitefox.web :as web] 6 | [sitefox.html :refer [render-into]] 7 | [sitefox.reloader :refer [nbb-reloader]])) 8 | 9 | (def t (fs/readFileSync "index.html")) 10 | 11 | (defn component-main [] 12 | [:div 13 | [:h1 "Hello world!"] 14 | [:p "This is my content."]]) 15 | 16 | (defn setup-routes [app] 17 | (web/reset-routes app) 18 | (web/static-folder app "/css" "node_modules/minimal-stylesheet/") 19 | (.get app "/" 20 | (fn [_req res] 21 | (->> (render-into t "main" [component-main]) 22 | (.send res))))) 23 | 24 | (defonce init 25 | (p/let [self *file* 26 | [app host port] (web/start)] 27 | (setup-routes app) 28 | (nbb-reloader self #(setup-routes app)) 29 | (println "Serving on" (str "https://" host ":" port)))) 30 | -------------------------------------------------------------------------------- /create/sitefox-nbb/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const args = process.argv.slice(2); 4 | const execSync = require('child_process').execSync; 5 | const fs = require('fs-extra'); 6 | const replace = require('replace-in-file'); 7 | 8 | const name = args[0]; 9 | const dir = name && args[0].replace(/-/g, '_'); 10 | 11 | if (name) { 12 | console.log("Creating", name); 13 | fs.copySync(__dirname + "/template", name); 14 | fs.moveSync(name + "/gitignore", name + "/.gitignore"); 15 | replace.sync({ 16 | "files": [ 17 | args[0] + "/**/**", 18 | ], 19 | "from": "NAME", 20 | "to": name, 21 | "countMatches": true, 22 | }); 23 | console.log("\nOk, you are ready to roll:"); 24 | console.log("$ cd " + name); 25 | console.log("$ npm install"); 26 | console.log("$ npm run serve"); 27 | console.log("\nThen edit server.cljs\n"); 28 | } else { 29 | console.log("Usage: " + process.argv[1] + " APP-NAME"); 30 | } 31 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const args = process.argv.slice(2); 4 | const execSync = require('child_process').execSync; 5 | const fs = require('fs-extra'); 6 | const replace = require('replace-in-file'); 7 | 8 | const name = args[0]; 9 | const dir = name && args[0].replace(/-/g, '_'); 10 | 11 | if (name) { 12 | console.log("Creating", name); 13 | fs.copySync(__dirname + "/template", name); 14 | fs.moveSync(name + "/src/NAME", name + "/src/" + dir); 15 | replace.sync({ 16 | "files": [ 17 | args[0] + "/**/**", 18 | ], 19 | "from": new RegExp("NAME", "g"), 20 | "to": name, 21 | "countMatches": true, 22 | }); 23 | console.log(); 24 | console.log("Ok, you are ready to roll:"); 25 | console.log("$ cd " + name); 26 | console.log("$ npm install"); 27 | console.log("$ make watch"); 28 | console.log(""); 29 | console.log("Then edit src/" + name + "/*.cljs"); 30 | console.log(); 31 | } else { 32 | console.log("Usage: " + process.argv[1] + " APP-NAME"); 33 | } 34 | -------------------------------------------------------------------------------- /src/sitefox/util.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.util 2 | "Various utilities for server side ClojureScript." 3 | (:require 4 | ["json-stringify-safe" :as json-stringify-safe] 5 | [sitefox.logging :refer [bail]])) 6 | 7 | (defn env 8 | "Returns the environment variable named in k with optional default value." 9 | [k & [default]] 10 | (or (aget js/process.env k) default)) 11 | 12 | (defn env-required 13 | "Returns the environment variable named in k or exits the process if missing. 14 | The message printed on exit can be customised in msg." 15 | [k & [msg]] 16 | (or (env k) (bail (or msg (str "Required environment variable is missing:" k))))) 17 | 18 | (defn error-to-json 19 | "Convert a JavaScript error into a form that can be returned as JSON data." 20 | [err] 21 | (let [e (js/JSON.parse (json-stringify-safe err))] 22 | (aset e "message" (str err)) 23 | #js {:error e})) 24 | 25 | (defn btoa 26 | "Server side version of browser JavaScript's btoa base64 encoding." 27 | [s] 28 | (-> s js/Buffer. (.toString "base64"))) 29 | -------------------------------------------------------------------------------- /examples/authentication/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sitefox HTML authentication example 6 | 7 | 8 | 9 | 16 | 17 | 18 |
19 |

Sitefox auth demo

20 |
21 |
22 |

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Chris McCormick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/nbb-auth/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sitefox HTML authentication example 6 | 7 | 8 | 9 | 17 | 18 | 19 |
20 |

Sitefox auth demo

21 |
22 |
23 |

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/Makefile: -------------------------------------------------------------------------------- 1 | STATIC=public/*.html public/css # public/images public/assets 2 | 3 | all: build build/server.js 4 | 5 | build/server.js: src/**/*.cljs shadow-cljs.edn node_modules 6 | npx shadow-cljs release server --debug 7 | git rev-parse HEAD | cut -b -8 > build/build-id.txt 8 | 9 | build: src/**/* $(STATIC) 10 | mkdir -p build/public 11 | cp -LR --preserve=all $(STATIC) build/public 12 | npx shadow-cljs release app 13 | touch build 14 | 15 | node_modules: package.json 16 | npm i 17 | touch node_modules 18 | 19 | .PHONY: watch watcher server repl clean 20 | 21 | server: node_modules 22 | @echo "waiting for devserver.js to appear." 23 | @rm -f devserver.js; until [ -f devserver.js -a -d .shadow-cljs ]; do sleep 1; done; echo "devserver.js appeared. starting." 24 | @sleep 1 && while [ 1 ]; do DEV=1 node devserver.js; sleep 3; echo "restarting devserver.js"; done 25 | 26 | watcher: node_modules 27 | npx shadow-cljs watch server app 28 | 29 | watch: 30 | make -j2 watcher server 31 | 32 | repl: 33 | npx shadow-cljs cljs-repl app 34 | 35 | clean: 36 | rm -rf .shadow-cljs devserver.js server.js build node_modules package-lock.json 37 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {thheller/shadow-cljs {:mvn/version "2.15.3"} 3 | funcool/promesa {:mvn/version "6.0.2"} 4 | applied-science/js-interop {:mvn/version "0.2.7"}} 5 | :aliases {:codox {:extra-deps {codox/codox {:mvn/version "0.10.8"}} 6 | :exec-fn codox.main/generate-docs 7 | :exec-args {:source-paths ["src"] 8 | :name "Sitefox" 9 | ;:namespaces [sitefox.web] 10 | ;:namespaces [sitefox.ui] 11 | ;:namespaces [sitefox.hello] 12 | ;:namespaces [sitefox.web sitefox.db sitefox.html sitefox.mail sitefox.logging sitefox.util sitefox.ui sitefox.reloader] 13 | :source-uri "https://github.com/chr15m/sitefox/blob/master/{filepath}#L{line}" 14 | :metadata {:doc/format :markdown} 15 | :output-path "docs" 16 | :reader :clojurescript 17 | :language :clojurescript 18 | :doc-files ["docs/README.md"]}}}} 19 | -------------------------------------------------------------------------------- /examples/send-email/email-example.cljs: -------------------------------------------------------------------------------- 1 | (ns email-example 2 | (:require 3 | [promesa.core :as p] 4 | [sitefox.html :refer [render]] 5 | [sitefox.mail :as mail])) 6 | 7 | (print "This demo uses ethereal.email unless SMTP_URL is configured.") 8 | (print "Results will be printed to the console and not sent.") 9 | (print) 10 | 11 | (p/do! 12 | (print "Sending a basic text email.") 13 | (-> (mail/send-email 14 | "test-to@example.com" 15 | "test@example.com" 16 | "This is my test email." 17 | :text "Hello, This is my first email from **Sitefox**. Thank you.") 18 | (.then js/console.log)) 19 | 20 | (print "Sending with multiple recipients, Reagent HTML, and custom X-Hello header.") 21 | (-> (mail/send-email 22 | ["test-to@example.com" 23 | "goober@goober.com"] 24 | "test@example.com" 25 | "This is my test email." 26 | :text "Hello, This is my second email from **Sitefox**. Thank you." 27 | :html (render [:p 28 | [:strong "Hello,"] [:br] 29 | "This is my second email from Sitefox." [:br] 30 | [:strong "Thank you."]]) 31 | :headers {:X-Hello "Hello world!"}) 32 | (.then js/console.log))) 33 | 34 | -------------------------------------------------------------------------------- /examples/nbb-auth/webserver.cljs: -------------------------------------------------------------------------------- 1 | (ns webserver 2 | (:require 3 | ["fs" :as fs] 4 | [promesa.core :as p] 5 | [applied-science.js-interop :as j] 6 | [nbb.core :refer [*file*]] 7 | [sitefox.reloader :refer [nbb-reloader]] 8 | [sitefox.web :as web] 9 | [sitefox.html :as html] 10 | [sitefox.auth :as auth])) 11 | 12 | (defn homepage [req res template] 13 | (p/let [user (j/get req :user)] 14 | (html/direct-to-template 15 | res template "main" 16 | [:div 17 | [:h3 "Homepage"] 18 | (if user 19 | [:<> 20 | [:p "Signed in as " (j/get-in user [:auth :email])] 21 | [:p [:a {:href (web/get-named-route req "auth:sign-out")} "Sign out"]]] 22 | [:p [:a {:href (web/get-named-route req "auth:sign-in")} "Sign in"]])]))) 23 | 24 | (defn setup-routes [app] 25 | (let [template (fs/readFileSync "index.html")] 26 | (web/reset-routes app) 27 | (auth/setup-auth app) 28 | (auth/setup-email-based-auth app template "main") 29 | (auth/setup-reset-password app template "main") 30 | (.get app "/" (fn [req res] (homepage req res template))))) 31 | 32 | (defonce init 33 | (p/let [self *file* 34 | [app host port] (web/start)] 35 | (setup-routes app) 36 | (nbb-reloader self #(setup-routes app)) 37 | (println "Serving on" (str "http://" host ":" port)))) 38 | -------------------------------------------------------------------------------- /examples/nbb-live-reloading/server.cljs: -------------------------------------------------------------------------------- 1 | (ns webserver 2 | (:require 3 | ["fs" :as fs] 4 | ["browser-sync" :as browser-sync] 5 | [promesa.core :as p] 6 | [nbb.core :refer [*file*]] 7 | [sitefox.web :as web] 8 | [sitefox.html :refer [render-into]] 9 | [sitefox.reloader :refer [nbb-reloader]])) 10 | 11 | (def template (fs/readFileSync "index.html")) 12 | 13 | (defn component-main [] 14 | [:div 15 | [:h1 "Your Sitefox site"] 16 | [:p "Welcome to your new sitefox site. 17 | The code for this site is in " [:code "server.cljs"] "."] 18 | [:p "Check out " 19 | [:a {:href "https://github.com/chr15m/sitefox#batteries-included"} "the documentation"] 20 | " to start building."]]) 21 | 22 | (defn setup-routes [app] 23 | (web/reset-routes app) 24 | (web/static-folder app "/css" "css/") 25 | (.get app "/" 26 | (fn [_req res] 27 | (->> (render-into template "main" [component-main]) 28 | (.send res))))) 29 | 30 | (defonce init 31 | (p/let [self *file* 32 | [app host port] (web/start) 33 | bs (browser-sync/init nil (clj->js {:files ["css/**/*.css"] 34 | :proxy (str host ":" port)}))] 35 | (setup-routes app) 36 | (nbb-reloader self (fn [] 37 | (setup-routes app) 38 | (.reload bs))) 39 | (print "Serving at" (str host ":" port)))) 40 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | github.com style (c) Vasily Polovnyov 3 | */ 4 | 5 | .hljs { 6 | display: block; 7 | overflow-x: auto; 8 | padding: 0.5em; 9 | color: #333; 10 | background: #f8f8f8; 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-quote { 15 | color: #998; 16 | font-style: italic; 17 | } 18 | 19 | .hljs-keyword, 20 | .hljs-selector-tag, 21 | .hljs-subst { 22 | color: #333; 23 | font-weight: bold; 24 | } 25 | 26 | .hljs-number, 27 | .hljs-literal, 28 | .hljs-variable, 29 | .hljs-template-variable, 30 | .hljs-tag .hljs-attr { 31 | color: #008080; 32 | } 33 | 34 | .hljs-string, 35 | .hljs-doctag { 36 | color: #d14; 37 | } 38 | 39 | .hljs-title, 40 | .hljs-section, 41 | .hljs-selector-id { 42 | color: #900; 43 | font-weight: bold; 44 | } 45 | 46 | .hljs-subst { 47 | font-weight: normal; 48 | } 49 | 50 | .hljs-type, 51 | .hljs-class .hljs-title { 52 | color: #458; 53 | font-weight: bold; 54 | } 55 | 56 | .hljs-tag, 57 | .hljs-name, 58 | .hljs-attribute { 59 | color: #000080; 60 | font-weight: normal; 61 | } 62 | 63 | .hljs-regexp, 64 | .hljs-link { 65 | color: #009926; 66 | } 67 | 68 | .hljs-symbol, 69 | .hljs-bullet { 70 | color: #990073; 71 | } 72 | 73 | .hljs-built_in, 74 | .hljs-builtin-name { 75 | color: #0086b3; 76 | } 77 | 78 | .hljs-meta { 79 | color: #999; 80 | font-weight: bold; 81 | } 82 | 83 | .hljs-deletion { 84 | background: #fdd; 85 | } 86 | 87 | .hljs-addition { 88 | background: #dfd; 89 | } 90 | 91 | .hljs-emphasis { 92 | font-style: italic; 93 | } 94 | 95 | .hljs-strong { 96 | font-weight: bold; 97 | } 98 | -------------------------------------------------------------------------------- /create/sitefox-fullstack/template/src/NAME/server.cljs: -------------------------------------------------------------------------------- 1 | (ns NAME.server 2 | (:require 3 | [applied-science.js-interop :as j] 4 | [promesa.core :as p] 5 | ["fs" :as fs] 6 | ["source-map-support" :as sourcemaps] 7 | [sitefox.html :refer [render-into]] 8 | [sitefox.web :as web] 9 | [sitefox.util :refer [env]] 10 | [sitefox.logging :refer [bind-console-to-file]] 11 | [sitefox.tracebacks :refer [install-traceback-handler]])) 12 | 13 | (bind-console-to-file) 14 | (sourcemaps/install) 15 | (let [admin-email (env "ADMIN_EMAIL") 16 | build-id (try (fs/readFileSync "build-id.txt") (catch :default _e "dev"))] 17 | (when admin-email 18 | (install-traceback-handler admin-email build-id))) 19 | 20 | (defonce server (atom nil)) 21 | 22 | (def template (fs/readFileSync "public/index.html")) 23 | 24 | (defn my-page [] 25 | [:main 26 | [:h1 "My page"] 27 | [:p "This is a server rendered page."] 28 | [:p [:a {:href "/"} "Return to the app"]]]) 29 | 30 | (defn api-example [_req res] 31 | (.json res (clj->js {:question 42}))) 32 | 33 | (defn setup-routes [app] 34 | (web/reset-routes app) 35 | (j/call app :get "/mypage" #(.send %2 (render-into template "body" [my-page]))) 36 | (j/call app :get "/api/example.json" api-example) 37 | (web/static-folder app "/" "public")) 38 | 39 | (defn main! [] 40 | (p/let [[app host port] (web/start)] 41 | (reset! server app) 42 | (setup-routes app) 43 | (println "Serving on" (str "http://" host ":" port)))) 44 | 45 | (defn ^:dev/after-load reload [] 46 | (js/console.log "Reloading.") 47 | (setup-routes @server)) 48 | -------------------------------------------------------------------------------- /create/sitefox-nbb/template/server.cljs: -------------------------------------------------------------------------------- 1 | (ns server 2 | (:require 3 | ["fs" :as fs] 4 | [promesa.core :as p] 5 | [nbb.core :refer [*file*]] 6 | ["browser-sync" :as browser-sync] 7 | ["fast-glob$default" :as fg] 8 | [sitefox.web :as web] 9 | [sitefox.util :refer [env]] 10 | [sitefox.html :refer [render-into]] 11 | [sitefox.reloader :refer [nbb-reloader]] 12 | [sitefox.tracebacks :refer [install-traceback-handler]])) 13 | 14 | (when-let [admin-email (env "ADMIN_EMAIL")] 15 | (install-traceback-handler admin-email)) 16 | 17 | (def template (fs/readFileSync "public/index.html")) 18 | 19 | (defn component-main [] 20 | [:div 21 | [:h1 "Your Sitefox site"] 22 | [:p "Welcome to your new sitefox site. 23 | The code for this site is in " [:code "server.cljs"] "."] 24 | [:p "Check out " 25 | [:a {:href "https://github.com/chr15m/sitefox#batteries-included"} "the documentation"] 26 | " to start building."]]) 27 | 28 | (defn setup-routes [app] 29 | (web/reset-routes app) 30 | (.get app "/" 31 | (fn [_req res] 32 | (->> (render-into template "main" [component-main]) 33 | (.send res)))) 34 | (web/static-folder app "/" "public")) 35 | 36 | (defonce init 37 | (p/let [self *file* 38 | [app host port] (web/start) 39 | sync-options {:files ["public/**/**"] 40 | :proxy (str host ":" port)} 41 | watch-files (fg #js [self "src/**/*.cljs" "public/*"]) 42 | bs (when (env "DEV") (browser-sync/init nil (clj->js sync-options)))] 43 | (setup-routes app) 44 | (nbb-reloader watch-files (fn [] 45 | (setup-routes app) 46 | (when bs (.reload bs)))) 47 | (print "Serving at" (str host ":" port)))) 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sitefox", 3 | "version": "0.0.26", 4 | "description": "Backend web framework for Node + ClojureScript", 5 | "author": "Chris McCormick ", 6 | "homepage": "https://github.com/chr15m/sitefox", 7 | "dependencies": { 8 | "@keyv/sqlite": "3.6.5", 9 | "cookie-parser": "1.4.5", 10 | "csrf-csrf": "3.0.3", 11 | "express": "4.21.0", 12 | "express-session": "1.17.2", 13 | "html-to-text": "8.1.0", 14 | "json-stringify-safe": "5.0.1", 15 | "keyv": "4.5.2", 16 | "morgan": "1.10.0", 17 | "node-html-parser": "4.1.5", 18 | "node-input-validator": "4.5.0", 19 | "nodemailer": "6.9.9", 20 | "passport": "0.6.0", 21 | "passport-local": "1.0.0", 22 | "react": "17.0.2", 23 | "react-dom": "17.0.2", 24 | "rotating-file-stream": "3.0.4", 25 | "source-map-support": "0.5.21", 26 | "tmp": "0.2.1" 27 | }, 28 | "devDependencies": { 29 | "@keyv/postgres": "1.4.10", 30 | "nbb": "1.2.182", 31 | "playwright": "1.30.0", 32 | "shadow-cljs": "2.19.0", 33 | "tree-kill": "1.2.2", 34 | "wait-port": "1.1.0" 35 | }, 36 | "scripts": { 37 | "sync-deps": "nbb bin/sync-deps.cljs", 38 | "docs": "nbb bin/generate-docs.cljs", 39 | "pre-publish": "npm run sync-deps; nbb bin/update-readme-versions.cljs; npm run docs; echo Changes:; git log --oneline `git rev-list --tags --max-count=1`..; echo; echo 'Now commit changes and run `git tag vX.Y.Z`.'", 40 | "test": "rm -f ./tests.sqlite; SECRET=testing TESTING=1 DATABASE_URL=sqlite://./tests.sqlite npx shadow-cljs compile test", 41 | "test-postgres": "bin/test-on-postgres", 42 | "test-e2e": "SECRET=testing NODE_OPTIONS='--experimental-fetch --no-warnings' nbb --classpath src src/sitefoxtest/e2etests.cljs", 43 | "watch": "SECRET=watching TESTING=1 shadow-cljs watch test" 44 | }, 45 | "files": [ 46 | "shadow-cljs.edn", 47 | "deps.edn", 48 | "src/**", 49 | "bin/**", 50 | "docs/**" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /examples/authentication/src/authexample/server.cljs: -------------------------------------------------------------------------------- 1 | (ns authexample.server 2 | (:require 3 | ["fs" :as fs] 4 | [applied-science.js-interop :as j] 5 | [promesa.core :as p] 6 | [sitefox.html :as html] 7 | [sitefox.web :as web] 8 | [sitefox.logging :refer [bind-console-to-file]] 9 | [sitefox.auth :as auth])) 10 | 11 | (bind-console-to-file) 12 | 13 | (defonce server (atom nil)) 14 | 15 | ; user-space calls 16 | 17 | (defn homepage [req res template] 18 | (p/let [user (j/get req :user)] 19 | (html/direct-to-template 20 | res template "main" 21 | [:div 22 | [:h3 "Homepage"] 23 | (if user 24 | [:<> 25 | [:p "Signed in as " (j/get-in user [:auth :email])] 26 | [:p [:a {:href (web/get-named-route req "auth:sign-out")} "Sign out"]]] 27 | [:p [:a {:href (web/get-named-route req "auth:sign-in")} "Sign in"]])]))) 28 | 29 | (defn setup-routes [app] 30 | (let [template (fs/readFileSync "index.html")] 31 | (web/reset-routes app) 32 | ; (j/call app :use (csrf-handler template "main")) ; view:simple-message 33 | (web/static-folder app "/css" "node_modules/minimal-stylesheet/") 34 | (auth/setup-auth app) ; optional argument `sign-out-redirect-url` which defaults to "/". 35 | (auth/setup-email-based-auth app template "main") 36 | (auth/setup-reset-password app template "main") 37 | #_ (setup-email-based-auth app template "main" 38 | :sign-in-redirect "/" 39 | :sign-in-form-component component:sign-in-form 40 | :sign-up-redirect "/" 41 | :sign-up-email-component component:sign-up-email 42 | :sign-up-email-subject "Please verify your email" 43 | :sign-up-from-address "no-reply@example.com" 44 | :sign-up-form-component component:sign-up-form 45 | :sign-up-form-done-component component:sign-up-form-done 46 | :simple-message-component component:simple-message) 47 | (j/call app :get "/" (fn [req res] (homepage req res template))))) 48 | 49 | (defn main! [] 50 | (p/let [[app host port] (web/start)] 51 | (reset! server app) 52 | (setup-routes app) 53 | (println "Server started on " (str host ":" port)))) 54 | 55 | (defn ^:dev/after-load reload [] 56 | (js/console.log "Reloading") 57 | (setup-routes @server)) 58 | -------------------------------------------------------------------------------- /src/sitefox/logging.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.logging 2 | "Functions to help with writing log files." 3 | (:require 4 | ["path" :refer [basename]] 5 | ["util" :as util] 6 | [applied-science.js-interop :as j] 7 | ["rotating-file-stream" :as rfs])) 8 | 9 | (defn flush-bound-console 10 | "This is deprecated in favour of `tracebacks/install-traceback-handler`." 11 | [cb] 12 | ; https://github.com/winstonjs/winston/issues/228 13 | (let [error-log (aget js/console "_logstream")] 14 | (if error-log 15 | (do 16 | (j/call error-log :on "finish" cb) 17 | (aset js/console "log" (fn [])) 18 | (aset js/console "error" (fn [])) 19 | (.end error-log)) 20 | (cb)))) 21 | 22 | (defn bind-console-to-file 23 | "This is deprecated in favour of `tracebacks/install-traceback-handler`. 24 | Rebinds `console.log` and `console.error` so that they write to `./logs/error.log` as well as stdout." 25 | [] 26 | (when (not (aget js/console "_logstream")) 27 | (let [logs (str (or js/__dirname ".") "/logs") 28 | error-log (.createStream rfs "error.log" (clj->js {:interval "7d" :path logs :teeToStdout true})) 29 | log-fn (fn [& args] 30 | (let [date (.toISOString (js/Date.)) 31 | [d t] (.split date "T") 32 | [t _] (.split t ".") 33 | out (str d " " t " " (apply util/format (clj->js args)) "\n")] 34 | (.write error-log out)))] 35 | (aset js/console "log" log-fn) 36 | (aset js/console "error" log-fn) 37 | (aset js/console "_logstream" error-log) 38 | ; make sure the final errors get caught 39 | (j/call js/process :on "unhandledRejection" 40 | (fn [reason p] 41 | (js/console.error reason "Unhandled Rejection at Promise" p) 42 | (flush-bound-console #(js/process.exit 1)))) 43 | (j/call js/process :on "uncaughtException" 44 | (fn [err] 45 | (js/console.error err) 46 | (flush-bound-console #(js/process.exit 1)))) 47 | log-fn))) 48 | 49 | (defn bail 50 | "Print a message and then kill the current process." 51 | [& msgs] 52 | (apply js/console.error msgs) 53 | (js/console.error "Server exit.") 54 | (flush-bound-console #(js/process.exit 1))) 55 | 56 | (defn now [] 57 | (-> (js/Date.) 58 | (.toISOString) 59 | (.split ".") 60 | first 61 | (.replace "T" " "))) 62 | 63 | (defn log 64 | "Console log with built-in file and time reference." 65 | [file-path & args] 66 | (apply print (conj (conj args (str (basename file-path) ":")) (now)))) 67 | 68 | -------------------------------------------------------------------------------- /src/sitefox/deps.cljc: -------------------------------------------------------------------------------- 1 | (ns sitefox.deps 2 | "This module exists so that shadow-cljs can be used without :target :esm. 3 | Nbb uses esm and so the $default syntax works there. 4 | With shadow-cljs in :target :node-script mode the imports can't have $default." 5 | #?(:cljs 6 | (:require 7 | #?(:org.babashka/nbb 8 | ["express$default" :as r-express] 9 | :cljs 10 | ["express" :as r-express]) 11 | #?(:org.babashka/nbb 12 | ["cookie-parser$default" :as r-cookies] 13 | :cljs 14 | ["cookie-parser" :as r-cookies]) 15 | #?(:org.babashka/nbb 16 | ["body-parser$default" :as r-body-parser] 17 | :cljs 18 | ["body-parser" :as r-body-parser]) 19 | #?(:org.babashka/nbb 20 | ["serve-static$default" :as r-serve-static] 21 | :cljs 22 | ["serve-static" :as r-serve-static]) 23 | #?(:org.babashka/nbb 24 | ["express-session$default" :as r-session] 25 | :cljs 26 | ["express-session" :as r-session]) 27 | #?(:org.babashka/nbb 28 | ["morgan$default" :as r-morgan] 29 | :cljs 30 | ["morgan" :as r-morgan]) 31 | #?(:org.babashka/nbb 32 | ["node-html-parser$default" :refer [parse]] 33 | :cljs 34 | ["node-html-parser" :refer [parse]]) 35 | #?(:org.babashka/nbb 36 | ["csrf-csrf" :refer [doubleCsrf]] 37 | :cljs 38 | ["csrf-csrf" :refer [doubleCsrf]]) 39 | #?(:org.babashka/nbb 40 | ["keyv$default" :as r-Keyv] 41 | :cljs 42 | ["keyv" :as r-Keyv]) 43 | #?(:org.babashka/nbb 44 | ["passport$default" :as r-passport] 45 | :cljs 46 | ["passport" :as r-passport]) 47 | #?(:org.babashka/nbb 48 | ["passport-local$default" :as r-LocalStrategy] 49 | :cljs 50 | ["passport-local" :as r-LocalStrategy]) 51 | #?(:org.babashka/nbb 52 | [nbb.core :refer [load-file]]) 53 | #?(:org.babashka/nbb 54 | ["fs" :refer [readFileSync]]))) 55 | #?(:clj 56 | (:refer-clojure :exclude [slurp]))) 57 | 58 | #?(:cljs 59 | (do 60 | (def express r-express) 61 | (def cookies r-cookies) 62 | (def body-parser r-body-parser) 63 | (def session r-session) 64 | (def csrf doubleCsrf) 65 | (def serve-static r-serve-static) 66 | (def morgan r-morgan) 67 | (def parse-html parse) 68 | (def Keyv r-Keyv) 69 | (def passport r-passport) 70 | (def LocalStrategy r-LocalStrategy) 71 | (def cljs-loader load-file))) 72 | 73 | #?(:clj 74 | (defmacro inline [file] 75 | (clojure.core/slurp file)) 76 | :org.babashka/nbb 77 | (defmacro inline [file] 78 | (.toString (readFileSync file)))) 79 | -------------------------------------------------------------------------------- /src/sitefox/reloader.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.reloader 2 | "Functions to ensuring live-reloading works during development mode." 3 | (:require 4 | [promesa.core :as p] 5 | [applied-science.js-interop :as j] 6 | [sitefox.util :refer [env]] 7 | [sitefox.deps :refer [cljs-loader]] 8 | [sitefox.ui :refer [log]])) 9 | 10 | (defn nbb-reloader 11 | "Sets up filewatcher for development. Automatically re-evaluates this 12 | file on changes. Uses browser-sync to push changes to the browser. 13 | `file` can be a path (string) or a vec of strings." 14 | [file callback] 15 | (when (or (env "DEV") (= (env "NODE_ENV") "development")) 16 | (p/let [watcher 17 | (-> (js/import "filewatcher") 18 | (.catch (fn [err] 19 | (println "Error while loading filewatcher.") 20 | (println "Try: npm install filewatcher --save-dev") 21 | (.log js/console err) 22 | (js/process.exit 1)))) 23 | watcher (.-default watcher) 24 | watcher (watcher) 25 | is-loading (atom false) 26 | on-change (fn on-change [file] 27 | (log "File changed:" file) 28 | (if-not @is-loading 29 | (-> (p/do! 30 | (println "Reloading!") 31 | (reset! is-loading true) 32 | (cljs-loader file) 33 | (callback) 34 | (println "Done reloading!")) 35 | (.catch (fn [err] 36 | (.log js/console err) 37 | (reset! is-loading false) 38 | true)) 39 | (.finally (fn [] 40 | (reset! is-loading false)))) 41 | (do (println "Load already in progress, retrying in 500ms") 42 | (js/setTimeout #(on-change file) 500))))] 43 | (if (= (type file) js/String) 44 | (.add watcher file) 45 | (doseq [f file] 46 | (.add watcher f))) 47 | (j/call watcher :on "change" (fn [file _stat] (on-change file))) 48 | (j/call watcher :on "fallback" 49 | (fn [limit] 50 | (print "Reloader hit file-watcher limit: " limit)))))) 51 | 52 | (defn sync-browser 53 | "Sets up browser-sync for development. Hot-loads CSS and automatically 54 | refreshes on server code change." 55 | [host port & [files]] 56 | (when (or (env "DEV") (= (env "NODE_ENV") "development")) 57 | (p/let [bs (-> (js/import "browser-sync") 58 | (.catch (fn [err] 59 | (println "Error while loading browser-sync.") 60 | (println "Try: npm install browser-sync --save-dev") 61 | (.log js/console err) 62 | (js/process.exit 1)))) 63 | init (aget bs "init")] 64 | (init nil (clj->js {:files (or files ["public"]) 65 | :proxy (str host ":" port)}))))) 66 | -------------------------------------------------------------------------------- /src/sitefox/tracebacks.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.tracebacks 2 | "Server side error handling. Get tracebacks from live sites emailed to you." 3 | (:require 4 | ["util" :as util] 5 | ["rotating-file-stream" :as rfs] 6 | ["source-map-support" :as source-map-support] 7 | [applied-science.js-interop :as j] 8 | [promesa.core :as p] 9 | [sitefox.mail :refer [send-email]] 10 | [sitefox.web :refer [build-absolute-uri]])) 11 | 12 | (defn ^:no-doc write-error-to-logfile [logfile & args] 13 | (let [date (.toISOString (js/Date.)) 14 | [d t] (.split date "T") 15 | [t _] (.split t ".") 16 | out (str d " " t " " (apply util/format (clj->js args)) "\n")] 17 | (.write logfile out))) 18 | 19 | (defn ^:no-doc flush-error-log [log] 20 | ; https://github.com/winstonjs/winston/issues/228 21 | (js/Promise. 22 | (fn [res _err] 23 | (if log 24 | (do 25 | (j/call log :on "finish" res) 26 | (.end log)) 27 | (res))))) 28 | 29 | (defn ^:no-doc handle-traceback [email-address log build-id error req] 30 | (p/let [error-message (str 31 | (if req 32 | (str "Sitefox traceback at " (build-absolute-uri req (aget req "path")) "\n") 33 | (str "Sitefox traceback at unknown URL \n")) 34 | (when build-id (str "Build: " build-id "\n")) 35 | "\n" 36 | (js->clj (or (aget error "stack") error)) 37 | "\n")] 38 | (js/console.error error-message) 39 | (js/console.log error-message) 40 | (when log 41 | (write-error-to-logfile log error-message)) 42 | (when email-address 43 | (send-email email-address email-address 44 | (if req 45 | (str "Sitefox traceback at " (build-absolute-uri req "/")) 46 | (str "Sitefox traceback")) 47 | :text error-message)))) 48 | 49 | (defn install-traceback-handler 50 | "Handle any unhandledRejections or uncaughtExceptions that occur outside request handlers. 51 | * If `email-address` is set, errors will be sent to the supplied address. 52 | * If `build-id` is set, it will be added to the log. 53 | 54 | Errors are also written to the rotated file `logs/error.log` and stderr. 55 | 56 | You can get a `build-id` using `git rev-parse HEAD | cut -b -8 > build-id.txt` and including it with `(rc/inline)`." 57 | [email-address & [build-id]] 58 | (let [sitefox-traceback-singleton (aget js/process "sitefox-traceback-handler")] 59 | (if (nil? sitefox-traceback-singleton) 60 | (let [log-dir (str (or js/__dirname ".") "/logs") ; (:file (meta #'f)) <- nbb 61 | log (.createStream rfs "error.log" (clj->js {:interval "7d" :path log-dir :teeToStdout true})) 62 | error-handler-fn (partial handle-traceback email-address log build-id) 63 | error-fn (fn [error] 64 | (p/do! 65 | (error-handler-fn error nil) 66 | (flush-error-log log) 67 | (js/process.exit 1)))] 68 | (.install source-map-support) 69 | (aset js/process "sitefox-traceback-handler" error-handler-fn) 70 | (.on js/process "unhandledRejection" error-fn) 71 | (.on js/process "uncaughtException" error-fn) 72 | error-handler-fn) 73 | sitefox-traceback-singleton))) 74 | -------------------------------------------------------------------------------- /docs/js/page_effects.js: -------------------------------------------------------------------------------- 1 | function visibleInParent(element) { 2 | var position = $(element).position().top 3 | return position > -50 && position < ($(element).offsetParent().height() - 50) 4 | } 5 | 6 | function hasFragment(link, fragment) { 7 | return $(link).attr("href").indexOf("#" + fragment) != -1 8 | } 9 | 10 | function findLinkByFragment(elements, fragment) { 11 | return $(elements).filter(function(i, e) { return hasFragment(e, fragment)}).first() 12 | } 13 | 14 | function scrollToCurrentVarLink(elements) { 15 | var elements = $(elements); 16 | var parent = elements.offsetParent(); 17 | 18 | if (elements.length == 0) return; 19 | 20 | var top = elements.first().position().top; 21 | var bottom = elements.last().position().top + elements.last().height(); 22 | 23 | if (top >= 0 && bottom <= parent.height()) return; 24 | 25 | if (top < 0) { 26 | parent.scrollTop(parent.scrollTop() + top); 27 | } 28 | else if (bottom > parent.height()) { 29 | parent.scrollTop(parent.scrollTop() + bottom - parent.height()); 30 | } 31 | } 32 | 33 | function setCurrentVarLink() { 34 | $('.secondary a').parent().removeClass('current') 35 | $('.anchor'). 36 | filter(function(index) { return visibleInParent(this) }). 37 | each(function(index, element) { 38 | findLinkByFragment(".secondary a", element.id). 39 | parent(). 40 | addClass('current') 41 | }); 42 | scrollToCurrentVarLink('.secondary .current'); 43 | } 44 | 45 | var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }()) 46 | 47 | function scrollPositionId(element) { 48 | var directory = window.location.href.replace(/[^\/]+\.html$/, '') 49 | return 'scroll::' + $(element).attr('id') + '::' + directory 50 | } 51 | 52 | function storeScrollPosition(element) { 53 | if (!hasStorage) return; 54 | localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft()) 55 | localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop()) 56 | } 57 | 58 | function recallScrollPosition(element) { 59 | if (!hasStorage) return; 60 | $(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x")) 61 | $(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y")) 62 | } 63 | 64 | function persistScrollPosition(element) { 65 | recallScrollPosition(element) 66 | $(element).scroll(function() { storeScrollPosition(element) }) 67 | } 68 | 69 | function sidebarContentWidth(element) { 70 | var widths = $(element).find('.inner').map(function() { return $(this).innerWidth() }) 71 | return Math.max.apply(Math, widths) 72 | } 73 | 74 | function calculateSize(width, snap, margin, minimum) { 75 | if (width == 0) { 76 | return 0 77 | } 78 | else { 79 | return Math.max(minimum, (Math.ceil(width / snap) * snap) + (margin * 2)) 80 | } 81 | } 82 | 83 | function resizeSidebars() { 84 | var primaryWidth = sidebarContentWidth('.primary') 85 | var secondaryWidth = 0 86 | 87 | if ($('.secondary').length != 0) { 88 | secondaryWidth = sidebarContentWidth('.secondary') 89 | } 90 | 91 | // snap to grid 92 | primaryWidth = calculateSize(primaryWidth, 32, 13, 160) 93 | secondaryWidth = calculateSize(secondaryWidth, 32, 13, 160) 94 | 95 | $('.primary').css('width', primaryWidth) 96 | $('.secondary').css('width', secondaryWidth).css('left', primaryWidth + 1) 97 | 98 | if (secondaryWidth > 0) { 99 | $('#content').css('left', primaryWidth + secondaryWidth + 2) 100 | } 101 | else { 102 | $('#content').css('left', primaryWidth + 1) 103 | } 104 | } 105 | 106 | $(window).ready(resizeSidebars) 107 | $(window).ready(setCurrentVarLink) 108 | $(window).ready(function() { persistScrollPosition('.primary')}) 109 | $(window).ready(function() { 110 | $('#content').scroll(setCurrentVarLink) 111 | $(window).resize(setCurrentVarLink) 112 | }) 113 | -------------------------------------------------------------------------------- /docs/.html: -------------------------------------------------------------------------------- 1 | 3 | documentation

-------------------------------------------------------------------------------- /docs/sitefox.deps.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.deps documentation

sitefox.deps

-------------------------------------------------------------------------------- /src/sitefox/mail.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.mail 2 | "Functions for sending email from web services using node-mailer." 3 | (:require 4 | [promesa.core :as p] 5 | [applied-science.js-interop :as j] 6 | [sitefox.util :refer [env]] 7 | ["fs" :as fs] 8 | ["nodemailer" :as nm] 9 | ["rotating-file-stream" :as rfs] 10 | ["path" :as path] 11 | ["tmp" :as tmp])) 12 | 13 | (def ^:no-doc server-dir (or js/__dirname "./")) 14 | 15 | (def console-transport 16 | (j/obj :sendMail 17 | (fn [mail] 18 | (j/let [text (j/get mail :text) 19 | html (j/get mail :html) 20 | text-file (.fileSync tmp #js {:postfix ".txt"}) 21 | html-file (.fileSync tmp #js {:postfix ".html"}) 22 | text-file-fd (j/get text-file :fd) 23 | html-file-fd (j/get html-file :fd)] 24 | (when text 25 | (.writeFileSync fs text-file-fd text) 26 | (j/assoc! mail :text (j/get text-file :name))) 27 | (.close fs text-file-fd) 28 | (when html 29 | (.writeFileSync fs html-file-fd html) 30 | (j/assoc! mail :html (j/get html-file :name))) 31 | (.close fs html-file-fd) 32 | (js/console.error "smtp-console-transport:" mail) 33 | (p/do! nil))))) 34 | 35 | (defn smtp-transport 36 | "Create the SMTP mail transport to be used by `send-email`. 37 | 38 | The `SMTP_SERVER` environment variable specifies the connection settings. 39 | If unset a test account at ethereal.email will be used." 40 | [] 41 | (let [smtp-url (env "SMTP_SERVER" nil)] 42 | (cond 43 | ; default to logging emails to console 44 | (nil? smtp-url) (js/Promise. (fn [res _err] (res console-transport))) 45 | ; if the user has specified to use ethereal mail 46 | (= (.toLowerCase smtp-url) "ethereal") 47 | (-> (.createTestAccount nm) 48 | (.then (fn [account] 49 | (.createTransport 50 | nm 51 | #js {:host "smtp.ethereal.email" 52 | :port 587 53 | :secure false 54 | :auth #js {:user (aget account "user") 55 | :pass (aget account "pass")}})))) 56 | ; if the user has specified an actual SMTP server 57 | :else (js/Promise. (fn [res _err] (res (.createTransport nm smtp-url))))))) 58 | 59 | (defn send-email 60 | "Send an email. 61 | 62 | Uses the `SMTP_SERVER` environment variable to configure the server to use for sending. 63 | For example: SMTP_SERVER=smtps://username:password@mail.someserver.com/?pool=true 64 | 65 | If you don't specify a server ethereal.email will be used and the viewing link will be returned in the `url` property of the result. 66 | You can use this for testing your emails in dev mode. 67 | 68 | * `to` and `from` are valid email addresses. 69 | * `to` can be an array of valid email addresses for multiple recipients. 70 | * `subject` is the text of the subject line. 71 | * Use `:text` for the body of the email in text format. 72 | * Use `:html` for the body of the email in html format. 73 | * Use `:smtp-transport` keyword argument if you want to re-use the transport to send multiple emails. 74 | * Use `:headers` to pass a map of additional headers." 75 | [to from subject & {:keys [transport headers text html]}] 76 | (p/let [transport (or transport (smtp-transport)) 77 | mail-params (clj->js (merge {:from from 78 | :to to 79 | :subject subject 80 | :text text 81 | :html html 82 | :headers headers})) 83 | send-result (-> (j/call transport :sendMail mail-params) 84 | (.catch (fn [err] (js/console.error err) err))) 85 | result-url (.getTestMessageUrl nm send-result) 86 | logs (path/join server-dir "/logs") 87 | mail-log (.createStream rfs "mail.log" #js {:interval "7d" :path logs}) 88 | log-result (-> 89 | (.createTransport nm #js {:jsonTransport true}) 90 | (j/call :sendMail mail-params)) 91 | log-entry (j/get log-result :message)] 92 | ;(js/console.log (j/get log-entry :message)) 93 | (when log-entry 94 | (j/call mail-log :write (str log-entry "\n"))) 95 | (when result-url 96 | (aset send-result "url" result-url)) 97 | (or send-result log-entry))) 98 | -------------------------------------------------------------------------------- /examples/form-validation/webserver.cljs: -------------------------------------------------------------------------------- 1 | (ns webserver 2 | (:require 3 | [promesa.core :as p] 4 | ["fs" :as fs] 5 | ["node-input-validator" :refer [Validator]] 6 | [nbb.core :refer [*file*]] 7 | [sitefox.html :refer [render-into]] 8 | [sitefox.web :as web] 9 | [sitefox.mail :as mail] 10 | [sitefox.reloader :refer [nbb-reloader]])) 11 | 12 | (def template (fs/readFileSync "index.html")) 13 | 14 | (def fields 15 | {:name ["required" "minLength:5" "maxLength:20"] 16 | :date ["required" "date"] 17 | :count ["required" "min:5" "max:10" "integer"]}) 18 | 19 | (def warnings 20 | {:name "You must enter a name between 5 and 20 characters." 21 | :date "You must enter a valid date in YYYY-MM-DD format." 22 | :count "You must enter a quantity between 5 and 10."}) 23 | 24 | (def button-script 25 | (str 26 | "ajax.onclick=()=>{ 27 | fetch('/_csrf-token').then(r=>r.json()).then((token)=>{ 28 | console.log('token: ', token); 29 | fetch('/ajax', 30 | {'method':'POST','body':'received!', 31 | 'headers':{'Content-Type':'text/plain','X-XSRF-TOKEN':token}} 32 | ).then(r=>r.text()).then(d=>{ajaxresult.innerHTML=d}) 33 | }); 34 | }")) 35 | 36 | (defn view:form [csrf-token data validation-errors] 37 | (let [ve (or validation-errors #js {}) 38 | data (or data #js {})] 39 | [:div 40 | [:h3 "Please fill out the form"] 41 | [:form {:method "POST"} 42 | [:p [:input.full {:name "name" :placeholder "Your name" :default-value (aget data "name")}]] 43 | (when (aget ve "name") 44 | [:p.warning (aget ve "name" "message")]) 45 | [:p [:input.full {:name "date" :placeholder "Today's date YYYY-MM-DD" :default-value (aget data "date")}]] 46 | (when (aget ve "date") 47 | [:p.warning (aget ve "date" "message")]) 48 | [:p [:input.full {:name "count" :placeholder "How many pets do you have?" :default-value (aget data "count")}]] 49 | (when (aget ve "count") 50 | [:p.warning (aget ve "count" "message")]) 51 | [:p [:input {:name "_csrf" :type "hidden" :default-value csrf-token}]] 52 | [:button {:type "submit"} "Submit"]] 53 | [:h3 "Fetch POST test"] 54 | [:div#ajaxresult] 55 | [:button#ajax "Send fetch request"] 56 | [:script {:dangerouslySetInnerHTML 57 | {:__html button-script}}]])) 58 | 59 | (defn view:thank-you [] 60 | [:div 61 | [:h3 "Form complete."] 62 | [:p "Thank you for filling out the form. It has been emailed home safely."]]) 63 | 64 | (defn validate-post-data [req] 65 | (p/let [data (aget req "body") 66 | validator (Validator. data (clj->js fields) (clj->js warnings)) 67 | validated (.check validator)] 68 | [data validated (aget validator "errors")])) 69 | 70 | (defn email-form [data] 71 | (-> (mail/send-email 72 | "test@example.com" 73 | "test@example.com" 74 | "Form results." 75 | :text (str 76 | "Here is the result of the form:\n\n" 77 | (js/JSON.stringify data nil 2))) 78 | (.then #(js/console.log "Email: " (aget % "url"))))) 79 | 80 | (defn serve-form [req res] 81 | (p/let [is-post (= (aget req "method") "POST") 82 | [data validated validation-errors] (when is-post (validate-post-data req)) 83 | passed-validation (and is-post validated) 84 | view (if passed-validation view:thank-you view:form) 85 | rendered-html (render-into template "main" [view (.csrfToken req) data validation-errors])] 86 | ; if the form was completed without errors send it 87 | (when passed-validation 88 | (print "Form validated. Sending email.") 89 | (email-form data)) 90 | (.send res rendered-html))) 91 | 92 | (defn handle-csrf-error [err _req res n] 93 | (if (= (aget err "code") "EBADCSRFTOKEN") 94 | (-> res 95 | (.status 403) 96 | (.send (render-into template "main" [:div.warning "The form was tampered with."]))) 97 | (n err))) 98 | 99 | (defn setup-routes [app] 100 | (web/reset-routes app) 101 | (web/static-folder app "/css" "node_modules/minimal-stylesheet/") 102 | (.use app handle-csrf-error) 103 | (.post app "/ajax" (fn [req res] (js/console.log (aget req "body")) (.send res (aget req "body")))) 104 | (.use app "/" serve-form)) 105 | 106 | (defonce serve 107 | (p/let [self *file* 108 | [app host port] (web/start)] 109 | (setup-routes app) 110 | (nbb-reloader self #(setup-routes app)) 111 | (println "Serving on" (str "http://" host ":" port)))) 112 | -------------------------------------------------------------------------------- /docs/sitefox.tracebacks.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.tracebacks documentation

sitefox.tracebacks

Server side error handling. Get tracebacks from live sites emailed to you.

4 |

install-traceback-handler

(install-traceback-handler email-address & [build-id])

Handle any unhandledRejections or uncaughtExceptions that occur outside request handlers. * If email-address is set, errors will be sent to the supplied address. * If build-id is set, it will be added to the log.

5 |

Errors are also written to the rotated file logs/error.log and stderr.

6 |

You can get a build-id using git rev-parse HEAD | cut -b -8 > build-id.txt and including it with (rc/inline).

7 |
-------------------------------------------------------------------------------- /docs/sitefox.reloader.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.reloader documentation

sitefox.reloader

Functions to ensuring live-reloading works during development mode.

4 |

nbb-reloader

(nbb-reloader file callback)

Sets up filewatcher for development. Automatically re-evaluates this file on changes. Uses browser-sync to push changes to the browser. file can be a path (string) or a vec of strings.

5 |

sync-browser

(sync-browser host port & [files])

Sets up browser-sync for development. Hot-loads CSS and automatically refreshes on server code change.

6 |
-------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # Lifted from github.com/squint-cljs/cherry 2 | 3 | name: Tests 4 | 5 | on: [push, pull_request] 6 | 7 | env: 8 | SECRET: testing 9 | TESTING: 1 10 | DATABASE_URL: sqlite://./tests.sqlite 11 | PORT: 8000 12 | TIMEOUT: 60 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macOS-latest] # windows-latest 19 | version: [16, 18, 20] 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - name: "Checkout code" 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.version }} 31 | 32 | - name: Prepare java 33 | uses: actions/setup-java@v4 34 | with: 35 | distribution: "adopt" 36 | java-version: 11 37 | 38 | - name: Setup Clojure 39 | uses: DeLaGuardo/setup-clojure@9.5 40 | with: 41 | cli: 1.10.3.1040 42 | 43 | - name: Cache clojure dependencies 44 | uses: actions/cache@v4 45 | with: 46 | path: | 47 | ~/.m2/repository 48 | ~/.gitlibs 49 | ~/.deps.clj 50 | # List all files containing dependencies: 51 | key: cljdeps-${{ hashFiles('shadow-cljs.edn') }} 52 | # key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }} 53 | # key: cljdeps-${{ hashFiles('project.clj') }} 54 | # key: cljdeps-${{ hashFiles('build.boot') }} 55 | restore-keys: cljdeps- 56 | 57 | - name: Install Node deps 58 | run: npm i 59 | 60 | # Main test suite 61 | 62 | - name: Run the tests 63 | run: npm run test 64 | 65 | - name: Run the e2e tests 66 | run: npm run test-e2e 67 | 68 | # Test suite with PostgreSQL as a db 69 | 70 | - name: Set up PostgreSQL 71 | run: | 72 | sudo apt update 73 | sudo apt install -y postgresql libpq-dev 74 | sudo service postgresql start 75 | sudo -u postgres createuser --superuser "$USER" 76 | if: runner.os == 'Linux' 77 | 78 | - name: Run the tests on Postgres 79 | run: npm run test-postgres 80 | if: runner.os == 'Linux' 81 | 82 | # npm init "create" script tests 83 | 84 | - name: Link local packages for create tests 85 | run: npm link 86 | 87 | - name: Create temporary directory for create tests 88 | run: mkdir test-create-scripts 89 | 90 | - name: Link fullstack create package for create tests 91 | run: cd create/sitefox-fullstack && npm link 92 | 93 | - name: Test npm create sitefox-fullstack 94 | working-directory: ./test-create-scripts 95 | run: | 96 | set -e 97 | # Use absolute path for npm create as we are in a different working dir 98 | npm create sitefox-fullstack test-fullstack-app --prefix ../ 99 | cd test-fullstack-app 100 | npm install 101 | make watch & 102 | bgpid=$! 103 | echo "Server PID: $bgpid" 104 | # Use double quotes for bash -c to allow $bgpid expansion 105 | if timeout $TIMEOUT bash -c \ 106 | "until echo > /dev/tcp/localhost/$PORT; do \ 107 | echo -n '.'; \ 108 | # Check if background process is still running 109 | kill -0 $bgpid 2>/dev/null || { echo 'Server died.'; exit 1; }; \ 110 | sleep 1; \ 111 | done 2>/dev/null"; then 112 | echo "-> CI saw server start successfully." 113 | else 114 | # Failure message updated slightly to reflect the new check 115 | echo "-> CI failed to see server." 116 | exit 1 # Exit with error if server fails to start 117 | fi 118 | echo "Attempting to kill processes after successful test..." 119 | pkill -f make || true 120 | pkill -f java || true 121 | pkill -f node || true 122 | sleep 2 # Give processes time to terminate gracefully 123 | # Verify port is free 124 | if lsof -i :$PORT; then 125 | echo "ERROR: Port $PORT still in use after kill attempts!" 126 | exit 1 127 | else 128 | echo "Port $PORT is free after kill attempts." 129 | fi 130 | 131 | - name: Link nbb create package for create tests 132 | run: cd create/sitefox-nbb && npm link 133 | 134 | - name: Test npm create sitefox-nbb 135 | working-directory: ./test-create-scripts 136 | run: | 137 | set -e 138 | # Use absolute path for npm create as we are in a different working dir 139 | npm create sitefox-nbb test-nbb-app --prefix ../ 140 | cd test-nbb-app 141 | npm install 142 | echo "Checking port $PORT before starting nbb server..." 143 | lsof -i :$PORT || echo "Port $PORT is free before nbb serve" 144 | npm run serve & 145 | bgpid=$! 146 | echo "Server PID: $bgpid" 147 | # Use double quotes for bash -c to allow $bgpid expansion 148 | if timeout $TIMEOUT bash -c \ 149 | "until echo > /dev/tcp/localhost/$PORT; do \ 150 | echo -n '.'; \ 151 | # Check if background process is still running 152 | kill -0 $bgpid 2>/dev/null || { echo 'Server died.'; exit 1; }; \ 153 | sleep 1; \ 154 | done 2>/dev/null"; then 155 | echo "-> CI saw server start successfully." 156 | else 157 | # Failure message updated slightly to reflect the new check 158 | echo "-> CI failed failed to see server." 159 | exit 1 # Exit with error if server fails to start 160 | fi 161 | pkill -f node || true 162 | sleep 2 # Give processes time to terminate gracefully 163 | -------------------------------------------------------------------------------- /src/sitefox/ui.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.ui 2 | "Utility functions that can be used in client-side code." 3 | (:require 4 | [clojure.test :refer [is]])) 5 | 6 | (defn log [& args] (apply js/console.log (clj->js args)) (first args)) 7 | 8 | (defn check-time-interval [seconds [divisor interval-name]] 9 | (let [interval (js/Math.floor (/ seconds divisor))] 10 | (when (> interval 1) 11 | (str interval " " interval-name)))) 12 | 13 | (defn time-since 14 | "Returns a string describing how long ago the date described in `date-string` was." 15 | {:test (fn [] 16 | (is (= (time-since (- (js/Date.) 18000)) "18 secs")) 17 | (is (= (time-since (- (js/Date.) (* 1000 60 60 25))) "25 hrs")) 18 | (is (= (time-since "2013-02-01T00:00:00.000Z" "2014-02-01T00:00:00.000Z") "12 months")) 19 | (is (= (time-since "2013-02-01T00:00:00.000Z" "2013-02-05T23:15:23.000Z") "4 days")) 20 | (is (nil? (time-since (+ (js/Date.) (* 1000 60 60 25))))) ; future or same dates return nil 21 | (is (nil? (time-since "2013-02-01T00:00:00.000Z" "2013-02-01T00:00:00.000Z"))))} 22 | [date-string & [from-date-string]] 23 | (let [from-date (if from-date-string (js/Date. from-date-string) (js/Date.)) 24 | since-epoch (-> date-string js/Date.) 25 | seconds (js/Math.floor (/ (- from-date since-epoch) 1000))] 26 | (first (remove nil? (map (partial check-time-interval seconds) 27 | [[31536000 "years"] 28 | [2592000 "months"] 29 | [86400 "days"] 30 | [3600 "hrs"] 31 | [60 "mins"] 32 | [1 "secs"]]))))) 33 | 34 | (defn simple-date-time 35 | "Returns a simple string representation of the date and time in YYYY-MM-DD HH:MM:SS format." 36 | {:test (fn [] 37 | (is (= (simple-date-time "2018-01-01T13:17:00.000Z") "2018-01-01 13:17:00")) 38 | (is (= (simple-date-time "2021-07-15T01:12:33.000Z") "2021-07-15 01:12:33")))} 39 | [dt] 40 | (-> dt (.split "T") (.join " ") (.split ".") first)) 41 | 42 | (def slug-regex (js/RegExp. "[^A-Za-z0-9\\u00C0-\\u1FFF\\u2800-\\uFFFD]+" "g")) 43 | 44 | (defn slug 45 | "Converts `text` to a url-friendly slug." 46 | {:test (fn [] 47 | (is (= (slug "A calm visit, to the kitchen.") "a-calm-visit-to-the-kitchen")) 48 | (is (= (slug "The 99th surprise.") "the-99th-surprise")) 49 | (is (= (slug "$ goober. it's true.") "goober-it-s-true")) 50 | (is (= (slug "* 我爱官话 something") "我爱官话-something")))} 51 | [text] 52 | (-> text 53 | .toString 54 | .toLowerCase 55 | (.replace slug-regex " ") 56 | .trim 57 | (.replace (js/RegExp. " " "g") "-"))) 58 | 59 | (defn get-cookie 60 | "Returns the value of the named cookie from js/document.cookies (or optionally the `cookies` argument)." 61 | {:test (fn [] 62 | (is (= (get-cookie "one" "one=two") "two")) 63 | (is (= (get-cookie "three" "three=four; five=six") "four")))} 64 | [cookie-name & [cookies]] 65 | (second (re-find (js/RegExp. (str cookie-name "=([^;]+)")) (or cookies (aget js/document "cookie"))))) 66 | 67 | (defn csrf-token 68 | "Returns the CSRF token passed by Sitefox in the `XSRF-TOKEN` cookie. 69 | Pass this value when doing POST requests via ajax: 70 | 71 | ```clojure 72 | (js/fetch \"/api/endpoint\" 73 | #js {:method \"POST\" 74 | :headers #js {:Content-Type \"application/json\" 75 | :XSRF-TOKEN (csrf-token)} 76 | :body (js/JSON.stringify (clj->js data))}) 77 | ``` 78 | 79 | **Note**: passing the token via client side cookie is now deprecated. 80 | If you want to re-enable it set the client side environment variable `SEND_CSRF_COOKIE`, 81 | and then this function will work again. 82 | 83 | See the README for more details: 84 | " 85 | [] 86 | (get-cookie "XSRF-TOKEN")) 87 | 88 | (defn fetch-csrf-token 89 | "Returns a promise which resolves to a string containing the CSRF token fetched from the Sitefox backend server. 90 | Pass this value as the X-XSRF-TOKEN when doing POST requests via ajax fetch: 91 | 92 | ```clojure 93 | (-> (fetch-csrf-token) 94 | (.then (fn [token] 95 | (js/fetch \"/api/endpoint\" 96 | #js {:method \"POST\" 97 | :headers #js {:Content-Type \"application/json\" 98 | :X-XSRF-TOKEN token} ; <- use token here 99 | :body (js/JSON.stringify (clj->js some-data))})))) 100 | ``` 101 | " 102 | [] 103 | (-> 104 | (js/fetch "/_csrf-token") 105 | (.then #(.json %)))) 106 | 107 | (defn json-post 108 | "Use HTTP post to send data to /url in JSON format. Will convert clj to js datastructures. 109 | Will obtain and use a CSRF token. Use `options` to override the options passed to `js/fetch`. 110 | The options will be shallow-merged into the defaults." 111 | [url data & [options]] 112 | (-> 113 | (fetch-csrf-token) 114 | (.then (fn [csrf-token] 115 | (js/fetch 116 | url 117 | (clj->js 118 | (merge 119 | {:method "POST" 120 | :headers {:content-type "application/json" 121 | :X-XSRF-TOKEN csrf-token} 122 | :credentials "include" 123 | :body (js/JSON.stringify (clj->js data))} 124 | (js->clj options :keywordize-keys true)))))) 125 | (.then (fn [res] (when (aget res "ok") (.json res)))))) 126 | -------------------------------------------------------------------------------- /docs/sitefox.util.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.util documentation

sitefox.util

Various utilities for server side ClojureScript.

4 |

btoa

(btoa s)

Server side version of browser JavaScript’s btoa base64 encoding.

5 |

env

(env k & [default])

Returns the environment variable named in k with optional default value.

6 |

env-required

(env-required k & [msg])

Returns the environment variable named in k or exits the process if missing. The message printed on exit can be customised in msg.

7 |

error-to-json

(error-to-json err)

Convert a JavaScript error into a form that can be returned as JSON data.

8 |
-------------------------------------------------------------------------------- /src/sitefox/html.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.html 2 | "Functions for wrangling HTML and rendering Reagent components into selectors." 3 | (:require 4 | [clojure.test :refer [is]] 5 | [applied-science.js-interop :as j] 6 | [reagent.dom.server :refer [render-to-static-markup] :rename {render-to-static-markup r}] 7 | [sitefox.deps :refer [parse-html]])) 8 | 9 | (defn parse "Shorthand for [`node-html-parser`'s `parse` function](https://www.npmjs.com/package/node-html-parser#usage). 10 | Returns a dom-like document object (HTMLElement) that can be manipulated as in the browser." 11 | [html-string] (parse-html html-string)) 12 | 13 | (defn $ "Shorthand for CSS style `querySelector` on parsed HTML `element` 14 | such as the `document` returned by the `parse` function or a sub-element." 15 | [element selector] (.querySelector element selector)) 16 | 17 | (defn $$ "Shorthand for CSS style `querySelectorAll` on parsed HTML `element` 18 | such as the `document` returned by the `parse` function or a sub-element." 19 | [element selector] (.querySelectorAll element selector)) 20 | 21 | (defn render "Shorthand for Reagent's `render-to-static-markup`." [form] (r form)) 22 | 23 | (defn render-anything 24 | "Render anything to HTML. 25 | If `source` is a Reagent form, `render-to-static-markup` is used. 26 | If `source` is a jsdom HTMLElement or other type of object `.toString` is used. 27 | If `source` is a fn it will be called with any args that were passed. 28 | If `source` is already a string it is passed through with no change." 29 | {:test (fn [] 30 | (let [string-html "
Hi
" 31 | el-html (parse string-html) 32 | reagent-html [:div {:id "thing"} "Hi"]] 33 | (is (= (render-anything string-html) string-html)) 34 | (is (= (render-anything el-html) string-html)) 35 | (is (= (render-anything reagent-html) string-html))))} 36 | [source & args] 37 | (cond 38 | (vector? source) (render source) 39 | (string? source) source 40 | (fn? source) (apply source args) 41 | :else (.toString source))) 42 | 43 | (defn select-apply 44 | "Parse `template` if it is a string and then run each of selector-applications on it. 45 | If it is already a `document`-like object it won't be parsed first. 46 | The `selector-applications` should each be an array like: `[selector document-method-name ...arguments]`. 47 | For each one the selector will be run and then the method run on the result, with arguments passed to the method. 48 | The special 'method' `setHTML` expects a Reagent form which will be rendered and `innerHTML` will be set to the result." 49 | {:test (fn [] 50 | (let [html-string "
"] 51 | (is (= (select-apply html-string ["#app" :remove]) 52 | "")) 53 | (is (= (select-apply html-string ["#app" :setHTML [:p "My message."]]) 54 | "

My message.

")) 55 | (is (= (select-apply html-string ["body" :setHTML "It's strong."]) 56 | "It's strong.")) 57 | (is (= (select-apply html-string ["span" :setHTML "In span."] ["#app" :remove]) 58 | "In span.In span.")) 59 | (is (= (select-apply html-string ["span" :setAttribute "data-thing" 42] ["#app" :remove]) 60 | "")) 61 | (is html-string)))} 62 | [template & selector-application-pairs] 63 | (let [string-template (= (type template) js/String) 64 | document (if string-template (parse-html template) template)] 65 | (doseq [[selector method-name & args] selector-application-pairs] 66 | (doseq [el ($$ document selector)] 67 | (if (= (keyword method-name) :setHTML) 68 | (j/assoc! el :innerHTML (render-anything (first args))) 69 | (j/apply el method-name (clj->js args))))) 70 | (if string-template 71 | (j/call document :toString) 72 | document))) 73 | 74 | (defn render-into 75 | "Render a Reagent component into the chosen element of an HTML document. 76 | 77 | * `html-string` is the HTML document to be modified. 78 | * `selector` is a CSS-style selector such as `#app` or `main`. 79 | * `reagent-forms` is a valid Reagent component." 80 | {:test (fn [] 81 | (let [html-string "
"] 82 | (is (render-into html-string "body" [:div "Hello, world!"])) 83 | (is (= (render-into html-string "#app" [:div "Hello, world!"]) 84 | "
Hello, world!
")) 85 | (is (= (render-into html-string "body" [:main "Hello, world!"]) 86 | "
Hello, world!
")) 87 | (is (thrown-with-msg? 88 | js/Error #"HTML element not found" 89 | (render-into html-string "#bad" [:div "Hello, world!"])))))} 90 | [html-string selector reagent-forms] 91 | (let [t (parse-html html-string) 92 | el ($ t selector) 93 | rendered (r reagent-forms)] 94 | (when (not el) (throw (js/Error. (str "HTML element not found: \"" selector "\"")))) 95 | (j/call el :set_content rendered) 96 | (.toString t))) 97 | 98 | (defn direct-to-template 99 | "Render `selector` `component` Reagent pairs into the HTML `template` string and use the express `res` to send the resulting HTML to the client." 100 | [res template & selector-component-pairs] 101 | (.send res 102 | (reduce 103 | (fn [html [selector component]] 104 | (render-into html selector component)) 105 | template 106 | (partition 2 selector-component-pairs)))) 107 | 108 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 73 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /docs/sitefox.logging.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.logging documentation

sitefox.logging

Functions to help with writing log files.

4 |

bail

(bail & msgs)

Print a message and then kill the current process.

5 |

bind-console-to-file

(bind-console-to-file)

This is deprecated in favour of tracebacks/install-traceback-handler. Rebinds console.log and console.error so that they write to ./logs/error.log as well as stdout.

6 |

flush-bound-console

(flush-bound-console cb)

This is deprecated in favour of tracebacks/install-traceback-handler.

7 |

log

(log file-path & args)

Console log with built-in file and time reference.

8 |

now

(now)
-------------------------------------------------------------------------------- /docs/sitefox.mail.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.mail documentation

sitefox.mail

Functions for sending email from web services using node-mailer.

4 |

console-transport

send-email

(send-email to from subject & {:keys [transport headers text html]})

Send an email.

5 |

Uses the SMTP_SERVER environment variable to configure the server to use for sending. For example: SMTP_SERVER=smtps://username:password@mail.someserver.com/?pool=true

6 |

If you don’t specify a server ethereal.email will be used and the viewing link will be returned in the url property of the result. You can use this for testing your emails in dev mode.

7 |
    8 |
  • to and from are valid email addresses.
  • 9 |
  • to can be an array of valid email addresses for multiple recipients.
  • 10 |
  • subject is the text of the subject line.
  • 11 |
  • Use :text for the body of the email in text format.
  • 12 |
  • Use :html for the body of the email in html format.
  • 13 |
  • Use :smtp-transport keyword argument if you want to re-use the transport to send multiple emails.
  • 14 |
  • Use :headers to pass a map of additional headers.
  • 15 |
16 |

smtp-transport

(smtp-transport)

Create the SMTP mail transport to be used by send-email.

17 |

The SMTP_SERVER environment variable specifies the connection settings. If unset a test account at ethereal.email will be used.

18 |
-------------------------------------------------------------------------------- /src/sitefox/db.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.db 2 | "Lightweight database access. 3 | The environment variable `DATABASE_URL` configures which database to connect to. 4 | 5 | By default it is set to use a local sqlite database: `sqlite://./database.sqlite` 6 | 7 | `DATABASE_URL` for Postgres: `postgresql://[user[:password]@][netloc][:port][,...][/dbname][?param1=value1&...]`" 8 | (:require 9 | [clojure.test :refer-macros [is async use-fixtures]] 10 | [promesa.core :as p] 11 | [applied-science.js-interop] 12 | [sitefox.util :refer [env]] 13 | [sitefox.deps :refer [Keyv]])) 14 | 15 | (def default-page-size 10) ; when iterating through results get this many at once 16 | (def database-url (env "DATABASE_URL" "sqlite://./database.sqlite")) 17 | 18 | (defn kv 19 | "Access a database backed key-value store ([Keyv](https://github.com/lukechilds/keyv) instance). 20 | `kv-ns` is a namespace (i.e. table). 21 | The database to use can be configured with the `DATABASE_URL` environment variable. 22 | See the Keyv documentation for details. 23 | The promise based API has methods like `(.set kv ...)` and `(.get kv ...)` which do what you'd expect." 24 | {:test (fn [] 25 | (when (env "TESTING") 26 | (async done 27 | (p/let [d (kv "tests") 28 | v #js [1 2 3] 29 | _ (.set d "hello" v) 30 | y (.get d "hello")] 31 | (is (= (js->clj v) (js->clj y)))) 32 | (done))))} 33 | [kv-ns] 34 | (Keyv. database-url (clj->js {:namespace kv-ns}))) 35 | 36 | (defn client 37 | "The database client that is connected to `DATABASE_URL`. 38 | This allows you to make raw queries against the database." 39 | {:test (fn [] 40 | (async done 41 | (p/let [c (client) 42 | dialect (aget c "opts" "dialect") 43 | version-fn (case dialect 44 | "sqlite" "sqlite_version()" 45 | "version()") 46 | v (.query c (str "SELECT " version-fn " AS v"))] 47 | (is (aget c "query")) 48 | (is (-> v (aget 0) (aget "v"))) 49 | (done))))} 50 | [] 51 | (-> 52 | (Keyv. database-url) 53 | (aget "opts" "store"))) 54 | 55 | ; recursive function to fill results with pages of filtered select rows 56 | (defn perform-select [c select-statement deserialize kv-ns pre page-size page filter-function results] 57 | (p/let [rows (.query c select-statement #js [kv-ns (or pre "") page-size (* page page-size)]) 58 | filter-function (or filter-function identity)] 59 | (doseq [row rows] 60 | (let [_k (aget row "key") ; TODO: include-keys should return [k v] tuples 61 | v (aget (deserialize (aget row "value")) "value")] 62 | (when (filter-function v) 63 | (.push results v)))) 64 | (if (<= (aget rows "length") 0) 65 | results 66 | (perform-select c select-statement deserialize kv-ns pre page-size (inc page) filter-function results)))) 67 | 68 | (defn ls 69 | "List all key-value entries matching a particular namespace and prefix. 70 | Returns a promise that resolves to rows of JSON objects containing the values. 71 | 72 | - `kv-ns` is the namespace/table name. 73 | - `pre` substring to filter key by i.e. keys matching `kv-ns:pre...`. 74 | - `db` an database handle (defaults to the one defined in `DATABASE_URL`). 75 | - `filter-function` filter every value through this function, removing falsy results." 76 | ; - `callback-function` instead of returning an array of results, fire a callback for every matching row. 77 | ; - `include-keys` return arrays of `[key, value]` instead of `value` objects. 78 | {:test (fn [] 79 | (when (env "TESTING") 80 | (async done 81 | (p/let [d (kv "tests") 82 | fixture [["first:a" 1] ["first:b" 2] ["second:c" 3] ["second:d" 4]] 83 | _ (p/all (map #(.set d (first %) (second %)) fixture)) 84 | one (ls "tests" "first") 85 | two (ls "tests" "second") 86 | one-test (set (map second (subvec fixture 0 2))) 87 | two-test (set (map second (subvec fixture 2 4)))] 88 | (is (= (set one) one-test)) 89 | (is (= (set two) two-test)) 90 | (.clear d) 91 | (done)))))} 92 | [kv-ns & [pre db filter-function]] 93 | (p/let [c (or db (client)) 94 | dialect (aget c "opts" "dialect") 95 | select-statement (case dialect 96 | "sqlite" "SELECT * FROM keyv WHERE key LIKE ? || ':' || ? || '%' LIMIT ? OFFSET ?" 97 | "SELECT * FROM keyv WHERE key LIKE $1 || ':' || $2 || '%' LIMIT $3 OFFSET $4") 98 | results #js []] 99 | (perform-select c select-statement (aget c "opts" "deserialize") kv-ns pre default-page-size 0 filter-function results))) 100 | 101 | (defn f 102 | "Filter all key-value entries matching a particular namespace and prefix, 103 | through the supplied" 104 | {:test (fn [] 105 | (when (env "TESTING") 106 | (async done 107 | (p/let [d (kv "tests") 108 | fixture [["first:a" 1] ["first:b" 2] ["first:c" 3] 109 | ["second:c" 3] ["second:d" 4] ["second:e" 5]] 110 | _ (p/all (map #(.set d (first %) (second %)) fixture)) 111 | filter-fn #(= (mod % 2) 1) 112 | one (f "tests" filter-fn "first") 113 | two (f "tests" filter-fn "second") 114 | one-test (set (filter filter-fn (map second (subvec fixture 0 3)))) 115 | two-test (set (filter filter-fn (map second (subvec fixture 3 6))))] 116 | (is (= (set one) one-test)) 117 | (is (= (set two) two-test)) 118 | (.clear d) 119 | (p/let [fixture (map (fn [i] 120 | [(str "item-" i) 121 | #js {:thingo i}]) 122 | (range 2000)) 123 | _ (p/all (map #(.set d (first %) (second %)) fixture)) 124 | filter-fn #(= (mod (aget % "thingo") 327) 1) 125 | results (f "tests" filter-fn)] 126 | (is (= (set (js->clj results :keywordize-keys true)) 127 | #{{:thingo 1} {:thingo 328} {:thingo 655} {:thingo 982} {:thingo 1309} {:thingo 1636} {:thingo 1963}}))) 128 | (.clear d) 129 | (done)))))} 130 | [kv-ns filter-function & [pre db]] 131 | (ls kv-ns pre db filter-function)) 132 | 133 | (defn ensure-local-postgres-db-exists 134 | "If DATABASE_URL is for a postgres url then create it if it does not already exist. 135 | An example of an on-disk db URL: 'postgres://%2Fvar%2Frun%2Fpostgresql/DBNAME'." 136 | [& [db-url]] 137 | (let [db-url (or db-url database-url)] 138 | (when (.startsWith (js/decodeURIComponent db-url) "postgres:///") 139 | (let [execSync (aget (js/require "child_process") "execSync") 140 | db-name (-> db-url (.split "/") last) 141 | cmd (str "psql '" db-name "' -c ';' 2>/dev/null && echo 'Database " db-name " exists.' || createdb '" db-name "'")] 142 | (execSync cmd #js {:shell true :stdio "inherit"}) 143 | ; connect once to initiate table creation 144 | (p/let [db (Keyv. db-url) 145 | query (aget db "opts" "store" "query") 146 | version-query (query "SELECT version();")] 147 | version-query))))) 148 | 149 | ; make sure postgres is set up to run the tests 150 | (use-fixtures 151 | :once {:before #(async done (p/do! (ensure-local-postgres-db-exists) (done))) 152 | #_#_ :after #(async done (done))}) 153 | -------------------------------------------------------------------------------- /docs/sitefox.db.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.db documentation

sitefox.db

Lightweight database access. The environment variable DATABASE_URL configures which database to connect to.

4 |

By default it is set to use a local sqlite database: sqlite://./database.sqlite

5 |

DATABASE_URL for Postgres: postgresql://[user[:password]@][netloc][:port][,...][/dbname][?param1=value1&...]

6 |

client

(client)

The database client that is connected to DATABASE_URL. This allows you to make raw queries against the database.

7 |

cljs-test-once-fixtures

database-url

default-page-size

ensure-local-postgres-db-exists

(ensure-local-postgres-db-exists & [db-url])

If DATABASE_URL is for a postgres url then create it if it does not already exist. An example of an on-disk db URL: ‘postgres://%2Fvar%2Frun%2Fpostgresql/DBNAME’.

8 |

f

(f kv-ns filter-function & [pre db])

Filter all key-value entries matching a particular namespace and prefix, through the supplied

9 |

kv

(kv kv-ns)

Access a database backed key-value store (Keyv instance). kv-ns is a namespace (i.e. table). The database to use can be configured with the DATABASE_URL environment variable. See the Keyv documentation for details. The promise based API has methods like (.set kv ...) and (.get kv ...) which do what you’d expect.

10 |

ls

(ls kv-ns & [pre db filter-function])

List all key-value entries matching a particular namespace and prefix. Returns a promise that resolves to rows of JSON objects containing the values.

11 |
    12 |
  • kv-ns is the namespace/table name.
  • 13 |
  • pre substring to filter key by i.e. keys matching kv-ns:pre....
  • 14 |
  • db an database handle (defaults to the one defined in DATABASE_URL).
  • 15 |
  • filter-function filter every value through this function, removing falsy results.
  • 16 |
17 |

perform-select

(perform-select c select-statement deserialize kv-ns pre page-size page filter-function results)
-------------------------------------------------------------------------------- /docs/sitefox.html.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.html documentation

sitefox.html

Functions for wrangling HTML and rendering Reagent components into selectors.

4 |

$

($ element selector)

Shorthand for CSS style querySelector on parsed HTML element such as the document returned by the parse function or a sub-element.

5 |

$$

($$ element selector)

Shorthand for CSS style querySelectorAll on parsed HTML element such as the document returned by the parse function or a sub-element.

6 |

direct-to-template

(direct-to-template res template & selector-component-pairs)

Render selector component Reagent pairs into the HTML template string and use the express res to send the resulting HTML to the client.

7 |

parse

(parse html-string)

Shorthand for node-html-parser’s parse function. Returns a dom-like document object (HTMLElement) that can be manipulated as in the browser.

8 |

render

(render form)

Shorthand for Reagent’s render-to-static-markup.

9 |

render-anything

(render-anything source & args)

Render anything to HTML. If source is a Reagent form, render-to-static-markup is used. If source is a jsdom HTMLElement or other type of object .toString is used. If source is a fn it will be called with any args that were passed. If source is already a string it is passed through with no change.

10 |

render-into

(render-into html-string selector reagent-forms)

Render a Reagent component into the chosen element of an HTML document.

11 |
    12 |
  • html-string is the HTML document to be modified.
  • 13 |
  • selector is a CSS-style selector such as #app or main.
  • 14 |
  • reagent-forms is a valid Reagent component.
  • 15 |
16 |

select-apply

(select-apply template & selector-application-pairs)

Parse template if it is a string and then run each of selector-applications on it. If it is already a document-like object it won’t be parsed first. The selector-applications should each be an array like: [selector document-method-name ...arguments]. For each one the selector will be run and then the method run on the result, with arguments passed to the method. The special ‘method’ setHTML expects a Reagent form which will be rendered and innerHTML will be set to the result.

17 |
-------------------------------------------------------------------------------- /docs/sitefox.ui.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.ui documentation

sitefox.ui

Utility functions that can be used in client-side code.

4 |

check-time-interval

(check-time-interval seconds [divisor interval-name])

csrf-token

(csrf-token)

Returns the CSRF token passed by Sitefox in the XSRF-TOKEN cookie. Pass this value when doing POST requests via ajax:

5 |
(js/fetch "/api/endpoint"
 6 |           #js {:method "POST"
 7 |                :headers #js {:Content-Type "application/json"
 8 |                              :XSRF-Token (csrf-token)}
 9 |                              :body (js/JSON.stringify (clj->js data))})
10 | 
11 |

Note: passing the token via client side cookie is now deprecated. If you want to re-enable it set the client side environment variable SEND_CSRF_COOKIE, and then this function will work again.

12 |

See the README for more details: https://github.com/chr15m/sitefox/#csrf-protection

13 |

fetch-csrf-token

(fetch-csrf-token)

Returns a promise which resolves to a string containing the CSRF token fetched from the Sitefox backend server. Pass this value as the X-XSRF-TOKEN when doing POST requests via ajax fetch:

14 |
  (-> (fetch-csrf-token)
15 |       (.then (fn [token]
16 |                (js/fetch "/api/endpoint"
17 |                          #js {:method "POST"
18 |                               :headers #js {:Content-Type "application/json"
19 |                                             :X-XSRF-TOKEN token} ; <- use token here
20 |                               :body (js/JSON.stringify (clj->js some-data))}))))
21 | 
22 |

json-post

(json-post url data & [options])

Use HTTP post to send data to /url in JSON format. Will convert clj to js datastructures. Will obtain and use a CSRF token. Use options to override the options passed to js/fetch. The options will be shallow-merged into the defaults.

24 |

log

(log & args)

simple-date-time

(simple-date-time dt)

Returns a simple string representation of the date and time in YYYY-MM-DD HH:MM:SS format.

25 |

slug

(slug text)

Converts text to a url-friendly slug.

26 |

slug-regex

time-since

(time-since date-string & [from-date-string])

Returns a string describing how long ago the date described in date-string was.

27 |
-------------------------------------------------------------------------------- /src/sitefox/web.cljs: -------------------------------------------------------------------------------- 1 | (ns sitefox.web 2 | "Functions to start the webserver and create routes." 3 | (:require 4 | [sitefox.util :refer [env]] 5 | [sitefox.db :as db] 6 | [promesa.core :as p] 7 | [applied-science.js-interop :as j] 8 | ["http" :as http] 9 | ["path" :as path] 10 | ["rotating-file-stream" :as rfs] 11 | ["express-session" :refer [Store]] 12 | ["express" :refer [Router]] 13 | [sitefox.deps :refer [express cookies body-parser serve-static session morgan csrf]] 14 | [sitefox.html :refer [direct-to-template]])) 15 | 16 | (def ^:no-doc server-dir (or js/__dirname "./")) 17 | 18 | (defn ^:no-doc create-store [kv] 19 | (let [e (Store.)] 20 | (aset e "destroy" (fn [sid callback] 21 | (p/let [result (j/call kv :delete sid)] 22 | (when callback (callback)) 23 | result))) 24 | (aset e "get" (fn [sid callback] 25 | (p/let [result (j/call kv :get sid)] 26 | (callback nil result) 27 | result))) 28 | (aset e "set" (fn [sid session callback] 29 | (p/let [result (j/call kv :set sid session)] 30 | (when callback (callback)) 31 | result))) 32 | (aset e "touch" (fn [sid session callback] 33 | (p/let [result (j/call kv :set sid session)] 34 | (when callback (callback)) 35 | result))) 36 | (aset e "clear" (fn [callback] 37 | (p/let [result (js/call kv :clear)] 38 | (when callback (callback)) 39 | result))) 40 | e)) 41 | 42 | (defn is-post? 43 | "Check whether an express request uses the POST method." 44 | [req] 45 | (= (aget req "method") "POST")) 46 | 47 | (defn setup-csrf-middleware 48 | [app] 49 | (let [pre-csrf-router (Router.)] 50 | (.use app pre-csrf-router) 51 | (j/assoc! app :pre-csrf-router pre-csrf-router)) 52 | (.use app (j/get (csrf #js {:getSecret (fn [] (env "SECRET" "DEVMODE")) 53 | :cookieOptions #js {:httpOnly true :sameSite "Strict" :secure true} 54 | :size 32 55 | :cookieName "XSRF-TOKEN" 56 | :getTokenFromRequest (fn [req] 57 | (or (j/get-in req [:body :_csrf]) 58 | (j/call req :get "xsrf-token") 59 | (j/call req :get "x-xsrf-token")))}) 60 | :doubleCsrfProtection)) 61 | (.get app "/_csrf-token" 62 | (fn [req res] 63 | (.json res (j/call req :csrfToken true false)))) 64 | (when (env "SEND_CSRF_COOKIE") 65 | (.use app 66 | (fn [req res done] 67 | (let [extension (.toLowerCase (path/extname (j/get req :path)))] 68 | (when (or (= extension "") 69 | (= extension ".html")) 70 | (let [token (j/call req :csrfToken true false)] 71 | (j/call res :cookie "XSRF-Token" token 72 | #js {:sameSite "Strict" :secure true})))) 73 | (done)))) 74 | app) 75 | 76 | (defn add-default-middleware 77 | "Set up default express middleware for: 78 | 79 | * Writing rotating logs to `logs/access.log`. 80 | * Setting up sessions in the configured database. 81 | * Parse cookies and body. 82 | 83 | Pass the `session-options` map to configure the `express-sessions` package. 84 | Sitefox defaults will be shallow-merged with the options map passed in." 85 | [app & [session-options]] 86 | ; emit a warning if SECRET is not set 87 | (when (nil? (env "SECRET")) (js/console.error "Warning: env var SECRET is not set.")) 88 | (let [logs (path/join server-dir "/logs") 89 | access-log (.createStream rfs "access.log" #js {:interval "7d" :path logs}) 90 | kv-session (db/kv "session") 91 | store (create-store kv-session)] 92 | ; set up sessions table 93 | (.use app 94 | (session 95 | (clj->js 96 | (merge 97 | {:secret (env "SECRET" "DEVMODE") 98 | :saveUninitialized true 99 | :resave true 100 | :cookie {:secure "auto" 101 | :httpOnly true 102 | ; 10 years 103 | :maxAge (* 10 365 24 60 60 1000)} 104 | :store store} 105 | (js->clj session-options :keywordize-keys true))))) 106 | ; set up logging 107 | (.use app (morgan "combined" #js {:stream access-log}))) 108 | ; configure sane server defaults 109 | (.set app "trust proxy" "loopback") 110 | (.use app (cookies (env "SECRET" "DEVMODE"))) 111 | ; json body parser 112 | (.use app (.json body-parser #js {:limit "10mb" :extended true :parameterLimit 1000})) 113 | (.use app (.urlencoded body-parser #js {:extended true})) 114 | (.use app (.text body-parser)) 115 | (setup-csrf-middleware app) 116 | app) 117 | 118 | (defn static-folder 119 | "Express middleware to statically serve a `dir` on a `route` relative to working dir." 120 | [app route dir] 121 | (.use app route (serve-static (path/join server-dir dir))) 122 | app) 123 | 124 | (defn reset-routes 125 | "Remove all routes in the current app and re-add the default middleware. 126 | Useful for hot-reloading code." 127 | [app] 128 | (let [router (aget app "_router")] 129 | (when router 130 | (js/console.error (str "Deleting " (aget router "stack" "length") " routes")) 131 | (aset app "_router" nil)) 132 | (add-default-middleware app))) 133 | 134 | (defn create 135 | "Create a new express app and add the default middleware." 136 | [] 137 | (-> (express) 138 | (add-default-middleware))) 139 | 140 | (defn serve 141 | "Start serving an express app. 142 | 143 | Configure `BIND_ADDRESS` and `PORT` with environment variables. 144 | They default to `127.0.0.1:8000`." 145 | [app] 146 | (let [host (env "BIND_ADDRESS" "127.0.0.1") 147 | port (env "PORT" "8000") 148 | server (.createServer http app)] 149 | (-> 150 | (js/Promise. 151 | (fn [res _err] 152 | (.listen server port host 153 | #(res [host port server]))))))) 154 | 155 | (defn start 156 | "Create a new express app and start serving it. 157 | Runs (create) and then (serve) on the result. 158 | Returns a promise which resolves with [app host port server] once the server is running." 159 | [] 160 | (let [app (create)] 161 | (-> 162 | (serve app) 163 | (.then (fn [[host port server]] [app host port server]))))) 164 | 165 | (defn build-absolute-uri 166 | "Creates an absolute URL including host and port. 167 | Use inside a route: `(build-absolute-uri req \"/somewhere\")`" 168 | [req path] 169 | (let [hostname (aget req "hostname") 170 | host (aget req "headers" "host")] 171 | (str (aget req "protocol") "://" 172 | (if (not= hostname "localhost") hostname host) 173 | (when (and path (not= (aget path 0) "/")) "/") 174 | path))) 175 | 176 | (defn strip-slash-redirect 177 | "Express middleware to strip slashes from the end of any URL by redirecting to the non-slash version. 178 | Use: `(.use app strip-slash-redirect)" 179 | [req res n] 180 | (let [path (aget req "path") 181 | url (aget req "url")] 182 | (if (and 183 | path 184 | (= (last path) "/") 185 | (> (aget path "length") 1)) 186 | (.redirect res 301 (str (.slice path 0 -1) (.slice url (aget path "length")))) 187 | (n)))) 188 | 189 | (defn name-route 190 | "Attach a name to a route that can be recalled with `get-named-route`." 191 | [app route route-name] 192 | (j/assoc-in! app [:named-routes route-name] route) 193 | route) 194 | 195 | (defn get-named-route 196 | "Retrieve a route that has previously been named." 197 | [req route-name] 198 | (j/get-in req [:app :named-routes route-name])) 199 | 200 | (defn setup-error-handler 201 | "Sets up an express route to handle 404 and 500 errors. 202 | This must be the last handler in your server: 203 | 204 | ```clojure 205 | (defn setup-routes [app] 206 | (web/reset-routes app) 207 | ; ... other routes 208 | ; 404 and 500 error handling 209 | (web/setup-error-handler app template \"main\" component-error-page email-error-callback)) 210 | ``` 211 | 212 | Pass it the express `app`, an HTML string `template`, query `selector` and a Reagent `view-component`. 213 | Optionally pass `error-handler-fn` which will be called on every 500 error with args `req`, `error`. 214 | The error `view-component` will be rendered and inserted into the template at `selector`. 215 | 216 | The error component will receive three arguments: 217 | * `req` - the express request object. 218 | * `error-code` - the exact error code that occurred (e.g. 404, 500 etc.). 219 | * `error` - the error object that was propagated (if any). 220 | 221 | Example: 222 | 223 | `(make-error-handler app my-template \"main\" my-error-component)` 224 | 225 | To have all 500 errors emailed to you and logged use `tracebacks/install-traceback-handler` and pass it as error-handler-fn." 226 | [app template selector view-component & [error-handler-fn]] 227 | ; handle 404 errors 228 | (j/call app :use 229 | (fn [req res] 230 | (-> res 231 | (.status 404) 232 | (direct-to-template template selector [view-component req 404])))) 233 | ; handle 500 errors 234 | (j/call app :use 235 | (fn [error req res done] 236 | (p/catch 237 | (p/let [_error-handler-result (when error-handler-fn 238 | (error-handler-fn error req))] 239 | (if (aget res "headersSent") 240 | (done error) 241 | (-> res 242 | (.status 500) 243 | ;(.json res (clj->js (js->clj error))) 244 | ;(.send (str "BADNESS " (aget req "path"))) 245 | (direct-to-template template selector [view-component req 500 error])))) 246 | (fn [error] 247 | (-> res 248 | (.status 500) 249 | (.type "text/plain") 250 | (.send (str "Error in error handler:\n\n" 251 | (.toString (or (aget error "stack") error)))))))))) 252 | -------------------------------------------------------------------------------- /docs/js/highlight.min.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.6.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||R(i))return i}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):E(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function g(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function h(e,n,t,r){var a=r?"":y.classPrefix,i='',i+n+o}function p(){var e,t,r,a;if(!E.k)return n(B);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)a+=n(B.substr(t,r.index-t)),e=g(E,r),e?(M+=e[1],a+=h(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return a+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?B+=n:(a.rE||a.eE||(B+=n),b(),a.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=i||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substr(O,I.index-O),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},a=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n,t,r,o,s,p=i(e);a(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("clojure",function(e){var t={"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},r="a-zA-Z_\\-!.?+*=<>&#'",n="["+r+"]["+r+"0-9/;:]*",a="[-+]?\\d+(\\.\\d+)?",o={b:n,r:0},s={cN:"number",b:a,r:0},i=e.inherit(e.QSM,{i:null}),c=e.C(";","$",{r:0}),d={cN:"literal",b:/\b(true|false|nil)\b/},l={b:"[\\[\\{]",e:"[\\]\\}]"},m={cN:"comment",b:"\\^"+n},p=e.C("\\^\\{","\\}"),u={cN:"symbol",b:"[:]{1,2}"+n},f={b:"\\(",e:"\\)"},h={eW:!0,r:0},y={k:t,l:n,cN:"name",b:n,starts:h},b=[f,i,m,p,c,u,l,s,d,o];return f.c=[e.C("comment",""),y,h],h.c=b,l.c=b,{aliases:["clj"],i:/\S/,c:[f,i,m,p,c,u,l,s,d]}});hljs.registerLanguage("clojure-repl",function(e){return{c:[{cN:"meta",b:/^([\w.-]+|\s*#_)=>/,starts:{e:/$/,sL:"clojure"}}]}}); -------------------------------------------------------------------------------- /docs/css/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 15px; 4 | } 5 | 6 | pre, code { 7 | font-family: Monaco, DejaVu Sans Mono, Consolas, monospace; 8 | font-size: 9pt; 9 | margin: 15px 0; 10 | } 11 | 12 | h1 { 13 | font-weight: normal; 14 | font-size: 29px; 15 | margin: 10px 0 2px 0; 16 | padding: 0; 17 | } 18 | 19 | h2 { 20 | font-weight: normal; 21 | font-size: 25px; 22 | } 23 | 24 | h5.license { 25 | margin: 9px 0 22px 0; 26 | color: #555; 27 | font-weight: normal; 28 | font-size: 12px; 29 | font-style: italic; 30 | } 31 | 32 | .document h1, .namespace-index h1 { 33 | font-size: 32px; 34 | margin-top: 12px; 35 | } 36 | 37 | #header, #content, .sidebar { 38 | position: fixed; 39 | } 40 | 41 | #header { 42 | top: 0; 43 | left: 0; 44 | right: 0; 45 | height: 22px; 46 | color: #f5f5f5; 47 | padding: 5px 7px; 48 | } 49 | 50 | #content { 51 | top: 32px; 52 | right: 0; 53 | bottom: 0; 54 | overflow: auto; 55 | background: #fff; 56 | color: #333; 57 | padding: 0 18px; 58 | } 59 | 60 | .sidebar { 61 | position: fixed; 62 | top: 32px; 63 | bottom: 0; 64 | overflow: auto; 65 | } 66 | 67 | .sidebar.primary { 68 | background: #e2e2e2; 69 | border-right: solid 1px #cccccc; 70 | left: 0; 71 | width: 250px; 72 | } 73 | 74 | .sidebar.secondary { 75 | background: #f2f2f2; 76 | border-right: solid 1px #d7d7d7; 77 | left: 251px; 78 | width: 200px; 79 | } 80 | 81 | #content.namespace-index, #content.document { 82 | left: 251px; 83 | } 84 | 85 | #content.namespace-docs { 86 | left: 452px; 87 | } 88 | 89 | #content.document { 90 | padding-bottom: 10%; 91 | } 92 | 93 | #header { 94 | background: #3f3f3f; 95 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); 96 | z-index: 100; 97 | } 98 | 99 | #header h1 { 100 | margin: 0; 101 | padding: 0; 102 | font-size: 18px; 103 | font-weight: lighter; 104 | text-shadow: -1px -1px 0px #333; 105 | } 106 | 107 | #header h1 .project-version { 108 | font-weight: normal; 109 | } 110 | 111 | .project-version { 112 | padding-left: 0.15em; 113 | } 114 | 115 | #header a, .sidebar a { 116 | display: block; 117 | text-decoration: none; 118 | } 119 | 120 | #header a { 121 | color: #f5f5f5; 122 | } 123 | 124 | .sidebar a { 125 | color: #333; 126 | } 127 | 128 | #header h2 { 129 | float: right; 130 | font-size: 9pt; 131 | font-weight: normal; 132 | margin: 4px 3px; 133 | padding: 0; 134 | color: #bbb; 135 | } 136 | 137 | #header h2 a { 138 | display: inline; 139 | } 140 | 141 | .sidebar h3 { 142 | margin: 0; 143 | padding: 10px 13px 0 13px; 144 | font-size: 19px; 145 | font-weight: lighter; 146 | } 147 | 148 | .sidebar h3 a { 149 | color: #444; 150 | } 151 | 152 | .sidebar h3.no-link { 153 | color: #636363; 154 | } 155 | 156 | .sidebar ul { 157 | padding: 7px 0 6px 0; 158 | margin: 0; 159 | } 160 | 161 | .sidebar ul.index-link { 162 | padding-bottom: 4px; 163 | } 164 | 165 | .sidebar li { 166 | display: block; 167 | vertical-align: middle; 168 | } 169 | 170 | .sidebar li a, .sidebar li .no-link { 171 | border-left: 3px solid transparent; 172 | padding: 0 10px; 173 | white-space: nowrap; 174 | } 175 | 176 | .sidebar li .no-link { 177 | display: block; 178 | color: #777; 179 | font-style: italic; 180 | } 181 | 182 | .sidebar li .inner { 183 | display: inline-block; 184 | padding-top: 7px; 185 | height: 24px; 186 | } 187 | 188 | .sidebar li a, .sidebar li .tree { 189 | height: 31px; 190 | } 191 | 192 | .depth-1 .inner { padding-left: 2px; } 193 | .depth-2 .inner { padding-left: 6px; } 194 | .depth-3 .inner { padding-left: 20px; } 195 | .depth-4 .inner { padding-left: 34px; } 196 | .depth-5 .inner { padding-left: 48px; } 197 | .depth-6 .inner { padding-left: 62px; } 198 | 199 | .sidebar li .tree { 200 | display: block; 201 | float: left; 202 | position: relative; 203 | top: -10px; 204 | margin: 0 4px 0 0; 205 | padding: 0; 206 | } 207 | 208 | .sidebar li.depth-1 .tree { 209 | display: none; 210 | } 211 | 212 | .sidebar li .tree .top, .sidebar li .tree .bottom { 213 | display: block; 214 | margin: 0; 215 | padding: 0; 216 | width: 7px; 217 | } 218 | 219 | .sidebar li .tree .top { 220 | border-left: 1px solid #aaa; 221 | border-bottom: 1px solid #aaa; 222 | height: 19px; 223 | } 224 | 225 | .sidebar li .tree .bottom { 226 | height: 22px; 227 | } 228 | 229 | .sidebar li.branch .tree .bottom { 230 | border-left: 1px solid #aaa; 231 | } 232 | 233 | .sidebar.primary li.current a { 234 | border-left: 3px solid #a33; 235 | color: #a33; 236 | } 237 | 238 | .sidebar.secondary li.current a { 239 | border-left: 3px solid #33a; 240 | color: #33a; 241 | } 242 | 243 | .namespace-index h2 { 244 | margin: 30px 0 0 0; 245 | } 246 | 247 | .namespace-index h3 { 248 | font-size: 16px; 249 | font-weight: bold; 250 | margin-bottom: 0; 251 | } 252 | 253 | .namespace-index .topics { 254 | padding-left: 30px; 255 | margin: 11px 0 0 0; 256 | } 257 | 258 | .namespace-index .topics li { 259 | padding: 5px 0; 260 | } 261 | 262 | .namespace-docs h3 { 263 | font-size: 18px; 264 | font-weight: bold; 265 | } 266 | 267 | .public h3 { 268 | margin: 0; 269 | float: left; 270 | } 271 | 272 | .usage { 273 | clear: both; 274 | } 275 | 276 | .public { 277 | margin: 0; 278 | border-top: 1px solid #e0e0e0; 279 | padding-top: 14px; 280 | padding-bottom: 6px; 281 | } 282 | 283 | .public:last-child { 284 | margin-bottom: 20%; 285 | } 286 | 287 | .members .public:last-child { 288 | margin-bottom: 0; 289 | } 290 | 291 | .members { 292 | margin: 15px 0; 293 | } 294 | 295 | .members h4 { 296 | color: #555; 297 | font-weight: normal; 298 | font-variant: small-caps; 299 | margin: 0 0 5px 0; 300 | } 301 | 302 | .members .inner { 303 | padding-top: 5px; 304 | padding-left: 12px; 305 | margin-top: 2px; 306 | margin-left: 7px; 307 | border-left: 1px solid #bbb; 308 | } 309 | 310 | #content .members .inner h3 { 311 | font-size: 12pt; 312 | } 313 | 314 | .members .public { 315 | border-top: none; 316 | margin-top: 0; 317 | padding-top: 6px; 318 | padding-bottom: 0; 319 | } 320 | 321 | .members .public:first-child { 322 | padding-top: 0; 323 | } 324 | 325 | h4.type, 326 | h4.dynamic, 327 | h4.added, 328 | h4.deprecated { 329 | float: left; 330 | margin: 3px 10px 15px 0; 331 | font-size: 15px; 332 | font-weight: bold; 333 | font-variant: small-caps; 334 | } 335 | 336 | .public h4.type, 337 | .public h4.dynamic, 338 | .public h4.added, 339 | .public h4.deprecated { 340 | font-size: 13px; 341 | font-weight: bold; 342 | margin: 3px 0 0 10px; 343 | } 344 | 345 | .members h4.type, 346 | .members h4.added, 347 | .members h4.deprecated { 348 | margin-top: 1px; 349 | } 350 | 351 | h4.type { 352 | color: #717171; 353 | } 354 | 355 | h4.dynamic { 356 | color: #9933aa; 357 | } 358 | 359 | h4.added { 360 | color: #508820; 361 | } 362 | 363 | h4.deprecated { 364 | color: #880000; 365 | } 366 | 367 | .namespace { 368 | margin-bottom: 30px; 369 | } 370 | 371 | .namespace:last-child { 372 | margin-bottom: 10%; 373 | } 374 | 375 | .index { 376 | padding: 0; 377 | font-size: 80%; 378 | margin: 15px 0; 379 | line-height: 16px; 380 | } 381 | 382 | .index * { 383 | display: inline; 384 | } 385 | 386 | .index p { 387 | padding-right: 3px; 388 | } 389 | 390 | .index li { 391 | padding-right: 5px; 392 | } 393 | 394 | .index ul { 395 | padding-left: 0; 396 | } 397 | 398 | .type-sig { 399 | clear: both; 400 | color: #088; 401 | } 402 | 403 | .type-sig pre { 404 | padding-top: 10px; 405 | margin: 0; 406 | } 407 | 408 | .usage code { 409 | display: block; 410 | color: #008; 411 | margin: 2px 0; 412 | } 413 | 414 | .usage code:first-child { 415 | padding-top: 10px; 416 | } 417 | 418 | p { 419 | margin: 15px 0; 420 | } 421 | 422 | .public p:first-child, .public pre.plaintext { 423 | margin-top: 12px; 424 | } 425 | 426 | .doc { 427 | margin: 0 0 26px 0; 428 | clear: both; 429 | } 430 | 431 | .public .doc { 432 | margin: 0; 433 | } 434 | 435 | .namespace-index .doc { 436 | margin-bottom: 20px; 437 | } 438 | 439 | .namespace-index .namespace .doc { 440 | margin-bottom: 10px; 441 | } 442 | 443 | .markdown p, .markdown li, .markdown dt, .markdown dd, .markdown td { 444 | line-height: 22px; 445 | } 446 | 447 | .markdown li { 448 | padding: 2px 0; 449 | } 450 | 451 | .markdown h2 { 452 | font-weight: normal; 453 | font-size: 25px; 454 | margin: 30px 0 10px 0; 455 | } 456 | 457 | .markdown h3 { 458 | font-weight: normal; 459 | font-size: 20px; 460 | margin: 30px 0 0 0; 461 | } 462 | 463 | .markdown h4 { 464 | font-size: 15px; 465 | margin: 22px 0 -4px 0; 466 | } 467 | 468 | .doc, .public, .namespace .index { 469 | max-width: 680px; 470 | overflow-x: visible; 471 | } 472 | 473 | .markdown pre > code { 474 | display: block; 475 | padding: 10px; 476 | } 477 | 478 | .markdown pre > code, .src-link a { 479 | border: 1px solid #e4e4e4; 480 | border-radius: 2px; 481 | } 482 | 483 | .markdown code:not(.hljs), .src-link a { 484 | background: #f6f6f6; 485 | } 486 | 487 | pre.deps { 488 | display: inline-block; 489 | margin: 0 10px; 490 | border: 1px solid #e4e4e4; 491 | border-radius: 2px; 492 | padding: 10px; 493 | background-color: #f6f6f6; 494 | } 495 | 496 | .markdown hr { 497 | border-style: solid; 498 | border-top: none; 499 | color: #ccc; 500 | } 501 | 502 | .doc ul, .doc ol { 503 | padding-left: 30px; 504 | } 505 | 506 | .doc table { 507 | border-collapse: collapse; 508 | margin: 0 10px; 509 | } 510 | 511 | .doc table td, .doc table th { 512 | border: 1px solid #dddddd; 513 | padding: 4px 6px; 514 | } 515 | 516 | .doc table th { 517 | background: #f2f2f2; 518 | } 519 | 520 | .doc dl { 521 | margin: 0 10px 20px 10px; 522 | } 523 | 524 | .doc dl dt { 525 | font-weight: bold; 526 | margin: 0; 527 | padding: 3px 0; 528 | border-bottom: 1px solid #ddd; 529 | } 530 | 531 | .doc dl dd { 532 | padding: 5px 0; 533 | margin: 0 0 5px 10px; 534 | } 535 | 536 | .doc abbr { 537 | border-bottom: 1px dotted #333; 538 | font-variant: none; 539 | cursor: help; 540 | } 541 | 542 | .src-link { 543 | margin-bottom: 15px; 544 | } 545 | 546 | .src-link a { 547 | font-size: 70%; 548 | padding: 1px 4px; 549 | text-decoration: none; 550 | color: #5555bb; 551 | } 552 | -------------------------------------------------------------------------------- /docs/sitefox.web.html: -------------------------------------------------------------------------------- 1 | 3 | sitefox.web documentation

sitefox.web

Functions to start the webserver and create routes.

4 |

add-default-middleware

(add-default-middleware app & [session-options])

Set up default express middleware for:

5 |
    6 |
  • Writing rotating logs to logs/access.log.
  • 7 |
  • Setting up sessions in the configured database.
  • 8 |
  • Parse cookies and body.
  • 9 |
10 |

Pass the session-options map to configure the express-sessions package. Sitefox defaults will be shallow-merged with the options map passed in.

11 |

build-absolute-uri

(build-absolute-uri req path)

Creates an absolute URL including host and port. Use inside a route: (build-absolute-uri req "/somewhere")

12 |

create

(create)

Create a new express app and add the default middleware.

13 |

get-named-route

(get-named-route req route-name)

Retrieve a route that has previously been named.

14 |

is-post?

(is-post? req)

Check whether an express request uses the POST method.

15 |

name-route

(name-route app route route-name)

Attach a name to a route that can be recalled with get-named-route.

16 |

reset-routes

(reset-routes app)

Remove all routes in the current app and re-add the default middleware. Useful for hot-reloading code.

17 |

serve

(serve app)

Start serving an express app.

18 |

Configure BIND_ADDRESS and PORT with environment variables. They default to 127.0.0.1:8000.

19 |

setup-error-handler

(setup-error-handler app template selector view-component & [error-handler-fn])

Sets up an express route to handle 404 and 500 errors. This must be the last handler in your server:

20 |
(defn setup-routes [app]
21 |   (web/reset-routes app)
22 |   ; ... other routes
23 |   ; 404 and 500 error handling
24 |   (web/setup-error-handler app template "main" component-error-page email-error-callback))
25 | 
26 |

Pass it the express app, an HTML string template, query selector and a Reagent view-component. Optionally pass error-handler-fn which will be called on every 500 error with args req, error. The error view-component will be rendered and inserted into the template at selector.

27 |

The error component will receive three arguments: * req - the express request object. * error-code - the exact error code that occurred (e.g. 404, 500 etc.). * error - the error object that was propagated (if any).

28 |

Example:

29 |

(make-error-handler app my-template "main" my-error-component)

30 |

To have all 500 errors emailed to you and logged use tracebacks/install-traceback-handler and pass it as error-handler-fn.

31 |

start

(start)

Create a new express app and start serving it. Runs (create) and then (serve) on the result. Returns a promise which resolves with app host port server once the server is running.

32 |

static-folder

(static-folder app route dir)

Express middleware to statically serve a dir on a route relative to working dir.

33 |

strip-slash-redirect

(strip-slash-redirect req res n)

Express middleware to strip slashes from the end of any URL by redirecting to the non-slash version. Use: `(.use app strip-slash-redirect)

34 |
--------------------------------------------------------------------------------