├── 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
--------------------------------------------------------------------------------
/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 | " "))
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 | ""))
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 |
--------------------------------------------------------------------------------
/docs/sitefox.mail.html:
--------------------------------------------------------------------------------
1 |
3 | sitefox.mail documentation sitefox.mail Functions for sending email from web services using node-mailer.
4 |
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 |
ensure-local-postgres-db-exists (ensure-local-postgres-db-exists & [db-url])
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 |
--------------------------------------------------------------------------------
/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 |
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 |
get-cookie (get-cookie cookie-name & [cookies])
Returns the value of the named cookie from js/document.cookies (or optionally the cookies argument).
23 |
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 |
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 |
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+=""+t(e)+">"}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 |
--------------------------------------------------------------------------------