├── example-worker ├── package.json ├── wrangler.toml ├── wrangler_tests.sh ├── project.clj ├── src │ └── my_worker │ │ └── core.cljs └── README.md ├── .github └── workflows │ ├── github-pages.yml │ └── clojurescript-package.yml ├── LICENSE ├── project.clj ├── README.md ├── src └── clojureworker │ └── core.cljs └── test └── clojureworker └── core_test.cljs /example-worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-worker", 3 | "main": "target/worker.js" 4 | } 5 | -------------------------------------------------------------------------------- /example-worker/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "test-worker" 2 | type = 'webpack' 3 | account_id = '' 4 | route = '' 5 | zone_id = '' 6 | usage_model = '' 7 | workers_dev = true 8 | target_type = "webpack" 9 | -------------------------------------------------------------------------------- /example-worker/wrangler_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | wrangler preview -u https://example.com/api/string-as-html --headless | grep 'cool response' 5 | wrangler preview -u https://example.com/api/map-as-json --headless | grep '{"hello":1}' 6 | wrangler preview -u https://example.com/api/ping --headless post ping! | grep 'ping!' 7 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Github Pages Docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - '**' 8 | jobs: 9 | build-and-publish-docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install deps 14 | run: | 15 | sudo apt update && sudo apt install -y leiningen 16 | - name: Build docs 17 | run: | 18 | lein codox 19 | - name: Publish 20 | uses: JamesIves/github-pages-deploy-action@4.1.4 21 | with: 22 | branch: gh-pages 23 | folder: target/doc 24 | -------------------------------------------------------------------------------- /example-worker/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example-worker "0.1.0-SNAPSHOT" 2 | :description "Example Clojure Cloudflare worker" 3 | :plugins [[lein-cljsbuild "1.1.8"]] 4 | :dependencies [[org.clojure/clojure "1.10.3"] 5 | [org.clojure/clojurescript "1.10.339"] 6 | [org.clojure/core.async "1.3.618"]] 7 | :source-paths ["src"] 8 | :hooks [leiningen.cljsbuild] 9 | :resource-paths ["../target/clojureworker-0.0.1.jar"] 10 | :cljsbuild { 11 | :builds {:production 12 | {:source-paths ["src"] 13 | :compiler {:output-to "target/worker.js" 14 | :optimizations :advanced}}}}) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jonas Otten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.github.sauercrowd/clojureworker "0.0.1" 2 | :description "Clojurescript library for Cloudflare Workers" 3 | :url "https://github.com/sauercrowd/clojureworker" 4 | :plugins [[lein-cljsbuild "1.1.8"] 5 | [lein-codox "0.10.7"]] 6 | :codox {:language :clojurescript} 7 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 8 | :url "https://www.eclipse.org/legal/epl-2.0/"} 9 | :dependencies [[org.clojure/clojure "1.10.3"] 10 | [org.clojure/core.async "1.3.618"] 11 | [org.clojure/clojurescript "1.10.339"]] 12 | :source-paths ["src"] 13 | :test-paths ["test"] 14 | :hooks [leiningen.cljsbuild] 15 | :cljsbuild { 16 | :test-commands {"unit-tests" ["node" "target/tests.js"]} 17 | :builds {:tests 18 | {:source-paths ["src" "test"] 19 | :notify-command ["node" "target/tests.js"] 20 | :compiler {:output-to "target/tests.js" 21 | :optimizations :none 22 | :target :nodejs 23 | :main clojureworker.core-test 24 | }} 25 | :production 26 | {:source-paths ["src"] 27 | :compiler {:output-to "target/output.js" 28 | :optimizations :advanced}}}}) 29 | -------------------------------------------------------------------------------- /example-worker/src/my_worker/core.cljs: -------------------------------------------------------------------------------- 1 | (ns my-worker.core 2 | (:require [clojureworker.core :as clfl])) 3 | 4 | ;; use simulate-worker to get a response returned by the function without any 5 | ;; browser dependencies 6 | 7 | (clfl/simulate-worker 8 | {:method "GET" :path "/test" :headers {} :body nil} 9 | (clfl/route "GET" "/test" (fn [req] 10 | (js/Promise. (fn [resolv cancel] 11 | (resolv {:body "promise resolved"}))))) 12 | (clfl/route "GET" "/api/string-as-html" "cool response") 13 | (clfl/route "GET" "/api/map-as-json" {:hello 1}) 14 | (clfl/route "POST" "/api/ping" #(identity {:body (:body %) 15 | :headers {} 16 | :status 200}))) 17 | 18 | 19 | ;; use worker to connect the routes to the worker environment 20 | (clfl/worker 21 | (clfl/route "GET" "/test" (fn [req] 22 | (js/Promise. (fn [resolv cancel] 23 | (resolv {:body "promise resolved"}))))) 24 | (clfl/route "GET" "/api/string-as-html" "cool response") 25 | (clfl/route "GET" "/api/map-as-json" {:hello 1}) 26 | (clfl/route "POST" "/api/ping" #(identity {:body (:body %) 27 | :headers {} 28 | :status 200}))) 29 | -------------------------------------------------------------------------------- /.github/workflows/clojurescript-package.yml: -------------------------------------------------------------------------------- 1 | name: clojureworker 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Install deps 11 | run: | 12 | sudo apt update && sudo apt install -y leiningen 13 | - name: lein build 14 | run: | 15 | lein cljsbuild once production 16 | test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install deps 21 | run: | 22 | sudo apt update && sudo apt install -y leiningen 23 | - name: lein test 24 | run: | 25 | lein test | grep "All tests succeeded!" 26 | create-worker: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Install deps 31 | run: | 32 | sudo apt update && sudo apt install -y leiningen 33 | - name: Get Wrangler 34 | run: 35 | cd example-worker && 36 | curl -L https://github.com/cloudflare/wrangler/releases/download/v1.17.0/wrangler-v1.17.0-x86_64-unknown-linux-musl.tar.gz -o ./wrangler.tar.gz && 37 | tar xvf wrangler.tar.gz 38 | - name: lein jar 39 | run: lein jar 40 | - name: compile example 41 | run: cd example-worker && lein compile 42 | - name: run worker and check response 43 | run: | 44 | cd example-worker && 45 | export PATH=$PATH:./dist && 46 | ./wrangler_tests.sh 47 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clojureworker 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/com.github.sauercrowd/clojureworker.svg)](https://clojars.org/com.github.sauercrowd/clojureworker) 4 | 5 | A Clojurescript library to simplify the use with Cloudflare workers, focusing on simplicity. 6 | ## Usage 7 | 8 | Clojureflare currently provides three functions, documented [here](https://cljdoc.org/d/com.github.sauercrowd/clojureworker/0.0.1/api/clojureworker.core). 9 | 10 | - `worker`, which routes as arguments and attaches the dispatcher to the worker event 11 | - `simulate-worker`, serving the same purpose. It acceps a request as a first argument and instead of responding to a worker event returns the response, which simplifies repl development. It avoids any dependencies that would limit the runtime to the browser. 12 | - `route`, creating a new route which expects three argument: `method` (e.g. GET, POST), `path` (such as /api/v1/ping) and handler (described below). 13 | 14 | ### The Handler 15 | 16 | The handler can currently be one of three types: 17 | - a string, which will generate a static HTML response 18 | - a map, which will generate a static JSON response 19 | - a function, which will receive the request as a parameter and returns a response map 20 | 21 | The request is of the form of 22 | 23 | ``` 24 | {:path "/test" :body "{\"key\":1}" :headers {}} 25 | ``` 26 | 27 | while the response map should similar to 28 | 29 | ``` 30 | {:body "{\"key\":1}" :headers {"Content-Type" "application/json"} :status 200} 31 | ``` 32 | 33 | Additionally the handler function can return a promise which eventually resolves to the described map. 34 | 35 | ### Example Worker 36 | 37 | Head over to the [example-worker](/example-worker) to create your first Clojurescript worker. 38 | -------------------------------------------------------------------------------- /example-worker/README.md: -------------------------------------------------------------------------------- 1 | # Example Worker 2 | 3 | This example worker will be used to briefly highlight the different pieces required to use Clojurescript with Cloudflare workers. 4 | It is somewhat opinionated (e.g. clojurescript build tool) but you should be able to swap these out as you go. 5 | 6 | As there are quite a few configuration files required I recommend using this directory as a template. 7 | 8 | ## Prerequisites 9 | 10 | - [leiningen](https://leiningen.org/) 11 | - [wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update) 12 | 13 | ## Bootstrap the project 14 | 15 | We'll start things of by creating the `project.clj` to define the structure of the Clojurescript project and how it should be build. 16 | 17 | ``` 18 | (defproject example-worker "0.1.0-SNAPSHOT" 19 | :description "Example Clojurescript Cloudflare worker" 20 | :plugins [[lein-cljsbuild "1.1.8"]] 21 | :dependencies [[org.clojure/clojure "1.10.3"] 22 | [org.clojure/clojurescript "1.10.339"] 23 | [com.github.sauercrowd/clojureworker "0.0.1"]] 24 | :source-paths ["src"] 25 | :hooks [leiningen.cljsbuild] 26 | :cljsbuild { 27 | :builds {:production 28 | {:source-paths ["src"] 29 | :compiler {:output-to "target/worker.js" 30 | :optimizations :advanced}}}}) 31 | ``` 32 | 33 | This project uses lein-cljsbuild to allow leiningen to build Clojurescript projects. We also define that the code is defined in the `src/` directory 34 | and that everything should be bundled up into the `target/worker.js` file, wich will be then passed to wrangler later on. 35 | We're using advanced optimizations but we shouldn't worry about that too much yet. 36 | 37 | ## Create the worker 38 | 39 | Next we'll get to the code, and luckily there's not a lot required. 40 | Create the file `src/new_worker/core.cljs` and it's respective directories. 41 | Now paste in 42 | 43 | ``` 44 | (ns new-worker.core 45 | (:require [clojureworker.core :as clfl])) 46 | 47 | (clfl/worker 48 | (clfl/route "GET" "/worker" "hello from my worker")) 49 | ``` 50 | 51 | That done we can now compile our worker into `target/worker.js` using 52 | 53 | ``` 54 | lein compile 55 | ``` 56 | 57 | ## Deploying with wrangler 58 | 59 | Next create a simple `package.json` outlining where te worker code can be found: 60 | 61 | ``` 62 | { 63 | "main": "target/worker.js" 64 | } 65 | ``` 66 | 67 | And now the wrangler configuration, `wrangler.toml`. To actually deploy it is it required to fill in the details as usually, but to run the preview we can leave the majority of fields empty: 68 | 69 | 70 | ``` 71 | name = "test-worker" 72 | type = 'webpack' 73 | account_id = '' 74 | route = '' 75 | zone_id = '' 76 | usage_model = '' 77 | workers_dev = true 78 | target_type = "webpack" 79 | ``` 80 | 81 | To now preview the worker run 82 | 83 | ``` 84 | $ wrangler preview -u https://example.com/worker --headless 85 | 86 | up to date, audited 1 package in 594ms 87 | 88 | found 0 vulnerabilities 89 | ⚠️ Your configuration file is missing the following fields: ["account_id"] 90 | ⚠️ Falling back to unauthenticated preview. 91 | 👷 Your Worker responded with: hello from my worker 92 | ``` 93 | 94 | That's all! 95 | 96 | Check out the [example clojure worker](src/my_worker/core.cljs) to see how promises and maps are returned, how you can provide a custom handler and how to use `simulate-worker` to ease the development process. 97 | 98 | 99 | Note: `wrangler_tests.sh` is part of the integration test pipeline, validating that the example worker can run and returns the expected responses. 100 | -------------------------------------------------------------------------------- /src/clojureworker/core.cljs: -------------------------------------------------------------------------------- 1 | (ns clojureworker.core 2 | (:require [cljs.core.async :refer [go chan !]] 3 | [cljs.core.async.interop :refer-macros [js {"status" (get resp :status 200) 24 | "headers" (get resp :headers {})}))) 25 | 26 | (defn ^:private get-key-from-req [req] 27 | (str (:method req) (:path req))) 28 | 29 | (defn ^:private assemble-handlers [routes] 30 | (into {} (map 31 | (juxt get-key-from-req identity) routes))) 32 | 33 | 34 | (defmulti ^:private routefn (fn [resp req] (type resp))) 35 | 36 | (defmethod ^:private routefn js/String [resp _] {:body resp :status 200 :headers {"Content-Type" "text/html"}}) 37 | 38 | (defmethod ^:private routefn PersistentArrayMap [resp _] 39 | {:body (.stringify js/JSON (clj->js resp)) 40 | :status 200 41 | :headers { "Content-Type" "application/json"}}) 42 | 43 | (defmethod ^:private routefn :default [resp req] (apply resp [req])) 44 | 45 | 46 | (defn ^:private handle-request [req routes] 47 | (let [rendered-routes (assemble-handlers routes) 48 | req-key (get-key-from-req req)] 49 | (if (contains? rendered-routes req-key) 50 | (routefn 51 | (:handler (get rendered-routes req-key)) req) 52 | {:status 404 :body "Not Found"}))) 53 | 54 | 55 | (defn ^:private extract-path [url] 56 | (.-pathname (js/URL. url))) 57 | 58 | 59 | (defn ^:private convert-request [req req-chan] 60 | (go 61 | (let [body (! req-chan 63 | {:path (extract-path (.-url req)) 64 | :method (.-method req) 65 | :headers (.-headers req) 66 | :body body})))) 67 | 68 | (defn ^:private worker-event-listener [req routes] 69 | (.respondWith req 70 | (js/Promise. (fn [resolv reject] 71 | (go 72 | (let [req-chan (chan) 73 | _ (convert-request (.-request req) req-chan) 74 | converted-req (!]] 5 | [cljs.core.async.interop :refer-macros [js {:userid 1 :score 5})) 75 | :status 200 :headers {"Content-Type" "application/json"}} 76 | _ (clojureworker.core/handle-request req routes)] 77 | (check-req routes req-ch expectation done)))) 78 | 79 | ; test if a function route 80 | (cljs.test/deftest test-fn-route 81 | (cljs.test/async done 82 | (let [req-ch (chan) 83 | req (clojureworker.core/convert-request #js {:url "http://localhost/api/v1/test" :method "GET" :text no-body-fn} req-ch) 84 | routes [(clojureworker.core/route "GET" "/api/v1/test" #(identity {:body "nice function" :status 200}))] 85 | expectation {:body "nice function" :status 200} 86 | _ (clojureworker.core/handle-request req routes)] 87 | (check-req routes req-ch expectation done)))) 88 | 89 | ; test if a function route with an arg 90 | (cljs.test/deftest test-fn-route-with-arg 91 | (cljs.test/async done 92 | (let [req-ch (chan) 93 | req (clojureworker.core/convert-request #js {:url "http://localhost/api/v1/test" :method "GET" :text no-body-fn} req-ch) 94 | routes [(clojureworker.core/route "GET" "/api/v1/test" #(identity 95 | {:body (str "nice function " (:path %)) :status 200}))] 96 | expectation {:body "nice function /api/v1/test" :status 200} 97 | _ (clojureworker.core/handle-request req routes)] 98 | (check-req routes req-ch expectation done)))) 99 | 100 | 101 | ; test if a function route with a promise response 102 | (cljs.test/deftest test-fn-route-with-promise 103 | (cljs.test/async done 104 | (let [req-ch (chan) 105 | req (clojureworker.core/convert-request #js {:url "http://localhost/api/v1/test" :method "GET" :text no-body-fn} req-ch) 106 | routes [(clojureworker.core/route "GET" "/api/v1/test" #(js/Promise. (fn [resolv reject] 107 | (resolv {:body (str "nice function " (:path %)) :status 200}))))] 108 | expectation {:body "nice function /api/v1/test" :status 200} 109 | _ (clojureworker.core/handle-request req routes)] 110 | (check-req routes req-ch expectation done)))) 111 | 112 | ;; test setup 113 | (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] 114 | (if (cljs.test/successful? m) 115 | (println "All tests succeeded!") 116 | (println "A test failed"))) 117 | 118 | (cljs.test/run-tests) 119 | --------------------------------------------------------------------------------