Reorganize build, tests and doc site

Tests and doc site can now run without node, using only
"lein figwheel". Tests and site are then re-run automatically
whenever a source file changes.

The doc site is now generated into "outsite/public", and can be
copied into the "gh-pages" branch with "make build-gh-pages".

"make push-gh-pages" builds the doc site and pushes it upstream
to the gh-pages branch there.

Generation of html pages is now handled completely in
ClojureScript, loaded from "bin/gen-site.js".

Link handling is a bit simplified.
This commit is contained in:
Dan Holmsand 2014-11-29 18:30:24 +01:00
parent 3bf98230ca
commit bffbae231d
24 changed files with 1168 additions and 341 deletions

6
.gitignore vendored
View File

@ -6,3 +6,9 @@ pom.xml
.lein-repl-history
pom.xml.asc
.nrepl-port
demo/empty.cljs
outsite/public/index.html
outsite/public/js
outsite/public/news
outsite/public/css
out

110
Makefile
View File

@ -1,51 +1,87 @@
PORT = 4562
PROF = dev
# PROF = prod,srcmap
# PROF = prod
CLJSBUILD = client
CLJSDIRS = src test
VERSION = 0.4.3
REACT_VERSION = 0.12.1
all: buildrun
PROF =
PORT = 3449
run: openbrowser buildrun
SITEDIR = outsite/public
OUTPUTDIR = $(SITEDIR)/js/out
leinbuild: setup
lein -o with-profile $(PROF) cljsbuild once $(CLJSBUILD)
openbrowser:
(sleep 1 && open site/test.html) &
# convenience shortcuts for continous building
##############################################
buildrun: setup
lein -o with-profile $(PROF) cljsbuild auto $(CLJSBUILD)
# development build with auto-reloading
run: figwheel
runtest:
$(MAKE) run PROF=test,$(PROF)
# development build with auto-reloading and site generation
runsite:
@$(MAKE) run PROF=dev,site,$(PROF)
runsite: setup
(sleep 3 && open "http://127.0.0.1:$(PORT)/$$(basename $$PWD)") &
# production build with auto-rebuild
runprod: clean
@$(MAKE) serve-site PROF=prod,$(PROF)
# production build with auto-rebuild and testing
runprodtest: clean
@$(MAKE) serve-site PROF=prod,test,$(PROF)
clean:
lein clean
## Subtargets
figwheel: trigger-build
@echo "Will start figwheel server at:\nhttp://127.0.0.1:$(PORT)\n\n"
lein with-profile $(PROF), figwheel
serve-site: trigger-build
@echo "Starting site at:\nhttp://127.0.0.1:$(PORT)/public\n\n"
( trap "kill 0" SIGINT SIGTERM EXIT; \
( cd .. && python -m SimpleHTTPServer $(PORT) & ); \
lein -o with-profile $(PROF),prod cljsbuild auto $(CLJSBUILD) )
( cd $(SITEDIR)/.. && python -m SimpleHTTPServer $(PORT) & ); \
lein with-profile $(PROF), cljsbuild auto )
install: leinbuild
lein install
trigger-build:
# always trigger build to make sure page-generation works
@echo "(ns empty.generated.ns)" > demo/empty.cljs
cat examples/todomvc/todos.css examples/simple/example.css \
> site/public/css/examples.css
preclean:
rm -rf repl .repl target out
mkdir -p vendor/reagent
clean: preclean
rm -rf news assets
lein -o clean
setup: preclean
mkdir -p news assets
## gh-pages support
###################
# build site and push upstream to the gh-pages branch
push-gh-pages: build-gh-pages
git push origin gh-pages
build-gh-pages: gen-site gh-pages-add
gen-site: clean
lein with-profile prod cljsbuild once
# copy contents of $(SITEDIR) to branch gh-pages
gh-pages-add:
# sanity check
test -f $(SITEDIR)/index.html
test ! -e $(OUTPUTDIR)
# make sure gh-pages branch exists
git show-branch gh-pages || true | git mktree | \
xargs git commit-tree | xargs git branch gh-pages
# clone gh-pages branch, and commit site to that
cd $(SITEDIR) && \
rm -rf .git tmp && \
git clone ../.. -lnb gh-pages tmp && \
mv tmp/.git . && \
git add . && git commit -m "Updated" && \
git push && rm -rf .git tmp
## Misc utilities
#################
show-outdated:
lein ancient :all
@ -56,14 +92,6 @@ download-react:
curl -L "http://fb.me/react-$(REACT_VERSION).min.js" \
-o vendor/reagent/react.min.js
gensite:
node bin/gen-site.js
demobuild:
$(MAKE) PROF=prod,demo leinbuild
buildsite: demobuild gensite
setversion:
version=$(VERSION); \
find . -name project.clj -o -name README.md | \

38
bin/cljs-load.js Normal file
View File

@ -0,0 +1,38 @@
var fs = require("fs");
var vm = require("vm");
var path = require("path");
var loadSrc = function (mainFile, outputDir, devModule) {
var src = fs.readFileSync(mainFile);
var googDir = path.join(outputDir, "goog");
var optNone = false;
if (outputDir) {
optNone = fs.existsSync(path.join(googDir, "deps.js"));
}
if (optNone) {
var cwd = process.cwd();
if (!global.goog) global.goog = {};
global.CLOSURE_IMPORT_SCRIPT = function (src) {
require(path.resolve(path.resolve(
cwd, path.join(googDir, src))));
return true;
};
var f = path.join(googDir, "base.js");
vm.runInThisContext(fs.readFileSync(f), f);
require(path.resolve(cwd, mainFile));
goog.require(devModule);
} else {
global.globalNodeRequire = require;
vm.runInThisContext("(function (require) {"
+ src
+ "\n})(globalNodeRequire);", mainFile);
}
return optNone;
};
exports.load = loadSrc;

View File

@ -1,53 +1,31 @@
#! /usr/bin/env node
var fs = require("fs");
var vm = require('vm');
var cljsLoad = require("./cljs-load");
var cssFiles = ['examples/todomvc/todos.css',
'examples/todomvc/todosanim.css',
'examples/simple/example.css',
'site/demo.css'];
var srcFile = "outsite/public/js/main.js";
var outputDirectory = "outsite/public/js/out/";
var moduleName = "devsetup";
var srcFile = "target/cljs-client.js";
var src = fs.readFileSync(srcFile);
var beep = "\u0007";
var clj_genpages = function (profile) {
if (typeof demo === 'undefined') {
vm.runInThisContext(src, srcFile);
}
return demo.genpages(profile);
var gensite = function () {
console.log("Loading " + srcFile);
var optNone = cljsLoad.load(srcFile, outputDirectory, moduleName);
sitetools.genpages({"opt-none": optNone});
}
var generate = function () {
var pages = clj_genpages();
Object.keys(pages).map(function (page) {
fs.writeFileSync(page, pages[page]);
});
fs.writeFileSync("assets/demo.js", src);
fs.writeFileSync("assets/demo.css",
cssFiles.map(function (x) {
return fs.readFileSync(x);
}).join("\n"));
console.log('Wrote site');
};
var compileOk = function () {
var msg = process.argv[2];
if (msg && msg.match(/failed/)) {
console.log("Compilation failed");
// beep
console.log('\u0007');
return false;
}
var compileFail = function () {
var msg = process.argv[process.argv.length - 1];
if (msg && msg.match(/failed/)) {
console.log("Compilation failed" + beep);
return true;
}
};
if (compileOk()) {
console.log('Writing site');
try {
generate();
} catch (e) {
console.log('\u0007');
console.error(e.stack);
}
if (!compileFail()) {
try {
gensite();
} catch (e) {
console.log(e + beep);
console.error(e.stack);
}
}

View File

@ -1,8 +1,8 @@
(ns demo
(:require [reagent.core :as reagent :refer [atom]]
[reagent.interop :as i :refer-macros [.' .! fvar]]
[reagent.interop :as i :refer-macros [.' .!]]
[clojure.string :as string]
[reagentdemo.page :as page :refer [page-map page link prefix]]
[sitetools :as tools :refer [link]]
[reagentdemo.common :as common :refer [demo-component]]
[reagentdemo.intro :as intro]
[reagentdemo.news :as news]
@ -10,9 +10,7 @@
(i/import-react)
(swap! page-map assoc
"index.html" (fvar intro/main)
"news/index.html" (fvar news/main))
(def test-results-comp (atom nil))
(def github {:href "https://github.com/reagent-project/reagent"})
@ -23,48 +21,26 @@
:alt "Fork me on GitHub"
:src "https://s3.amazonaws.com/github/ribbons/forkme_left_orange_ff7600.png"}]])
(def index-page "index.html")
(def news-page "news/index.html")
(tools/register-page index-page (fn [] [intro/main]))
(tools/register-page news-page (fn [] [news/main]))
(defn demo []
(dbg "demo")
[:div
[:div.nav
[:ul.nav
[:li.brand [link {:href (fvar intro/main)} "Reagent:"]]
[:li [link {:href (fvar intro/main)} "Intro"]]
[:li [link {:href (fvar news/main)} "News"]]
[:li.brand [link {:href index-page} "Reagent:"]]
[:li [link {:href index-page} "Intro"]]
[:li [link {:href news-page} "News"]]
[:li [:a github "GitHub"]]]]
(let [comp (get @page-map @page (fvar intro/main))]
[comp])
(when @test-results-comp [@test-results-comp])
[tools/page-content]
[github-badge]])
(defn ^:export mountdemo [p]
(when p (page/set-start-page p))
(reagent/render-component [demo] (.-body js/document)))
(defn gen-page [p timestamp]
(reset! page p)
(let [body (reagent/render-component-to-string [demo])
title @page/title-atom
load-page (case p "index.html" "" p)]
(str "<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<title>" title "</title>
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<link rel='stylesheet' href='" (prefix "assets/demo.css") timestamp "'>
</head>
<body>
" body "
<script type='text/javascript'
src='" (prefix "assets/demo.js") timestamp "'></script>
<script type='text/javascript'>
setTimeout(function() {demo.mountdemo('" load-page "')}, 200);
</script>
</body>
</html>")))
(defn ^:export genpages []
(let [timestamp (str "?" (.now js/Date))]
(->> (keys @page-map)
(map #(vector % (gen-page % timestamp)))
(into {})
clj->js)))
(defn start! [{:keys [test-results]}]
(reset! test-results-comp test-results)
(tools/start! {:body (fn [] [demo])}))

View File

@ -2,7 +2,6 @@
(:require [reagent.core :as reagent :refer [atom]]
[reagent.debug :refer-macros [dbg println]]
[clojure.string :as string]
[reagentdemo.page :as rpage]
[reagentdemo.syntax :as syntax]))
(def syntaxify (memoize syntax/syntaxify))

View File

@ -1,11 +1,11 @@
(ns reagentdemo.intro
(:require [reagent.core :as reagent :refer [atom]]
[reagent.interop :refer-macros [.' .! fvar]]
[reagent.interop :refer-macros [.' .!]]
[reagent.debug :refer-macros [dbg println]]
[clojure.string :as string]
[reagentdemo.syntax :refer-macros [get-source]]
[reagentdemo.page :refer [link title]]
[sitetools :refer [link title]]
[reagentdemo.common :as common :refer [demo-component]]
[simpleexample :as simple]
[todomvc :as todo]))

View File

@ -1,9 +1,9 @@
(ns reagentdemo.news
(:require [reagent.core :as reagent :refer [atom]]
[reagent.interop :refer-macros [.' .! fvar]]
[reagent.interop :refer-macros [.' .!]]
[reagent.debug :refer-macros [dbg println]]
[reagentdemo.page :refer [title link page-map]]
[reagentdemo.common :as common :refer [demo-component]]
[sitetools :as tools :refer [title link]]
[reagentdemo.news.anyargs :as anyargs]
[reagentdemo.news.async :as async]
[reagentdemo.news.undodemo :as undodemo]

View File

@ -1,12 +1,14 @@
(ns reagentdemo.news.anyargs
(:require [reagent.core :as r :refer [atom]]
[reagent.interop :refer-macros [.' .! fvar]]
[reagent.interop :refer-macros [.' .!]]
[reagent.debug :refer-macros [dbg println]]
[reagentdemo.syntax :refer-macros [get-source]]
[reagentdemo.page :refer [title link page-map]]
[sitetools :as tools :refer [title link]]
[reagentdemo.common :as common :refer [demo-component]]
[geometry.core :as geometry]))
(def url "news/any-arguments.html")
(def funmap (-> ::this get-source common/fun-map))
(def src-for (partial common/src-for funmap))
@ -35,7 +37,7 @@
geometry {:href "https://github.com/reagent-project/reagent/tree/master/examples/geometry"}
jonase {:href "https://github.com/jonase"}]
[:div.reagent-demo
[:h1 [link {:href (fvar main)} head]]
[:h1 [link {:href url} head]]
[title (str "Reagent 0.4.0: " head)]
[:div.demo-text
@ -53,12 +55,11 @@
them."]
(if summary
[link {:href (fvar main)
:class 'news-read-more} "Read more"]
[link {:href url :class 'news-read-more} "Read more"]
[:div.demo-text
[:p "In other words, you can now do this:"]
[demo-component {:comp (fvar say-hello)
[demo-component {:comp say-hello
:src (src-for [:hello-component :say-hello])}]
[:p "In the above example, it wouldnt make any difference at
@ -77,7 +78,7 @@
and " [:code "for"] " expressions, so its safest to always
put the call at the top, as in " [:code "my-div"] " here:"]
[demo-component {:comp (fvar call-my-div)
[demo-component {:comp call-my-div
:src (src-for [:nsr :my-div :call-my-div])}]
[:p [:em "Note: "] [:code "r/props"] " and "
@ -129,7 +130,6 @@
use Reagents new calling convensions, and looks like
this:"]
[demo-component {:comp (fvar geometry-example)}]])]]))
[demo-component {:comp geometry-example}]])]]))
(swap! page-map assoc
"news/any-arguments.html" (fvar main))
(tools/register-page url (fn [] [main]))

View File

@ -1,11 +1,13 @@
(ns reagentdemo.news.async
(:require [reagent.core :as reagent :refer [atom]]
[reagent.interop :refer-macros [.' .! fvar]]
[reagent.interop :refer-macros [.' .!]]
[reagent.debug :refer-macros [dbg println]]
[reagentdemo.syntax :refer-macros [get-source]]
[reagentdemo.page :refer [title link page-map]]
[sitetools :as tools :refer [title link]]
[reagentdemo.common :as common :refer [demo-component]]))
(def url "news/reagent-is-async.html")
(def funmap (-> "reagentdemo/news/async.cljs" get-source common/fun-map))
(def src-for (partial common/src-for funmap))
@ -92,7 +94,7 @@
(let [om-article {:href "http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs/"}]
[:div.reagent-demo
[title "Reagent: Faster by waiting"]
[:h1 [link {:href (fvar main)} "Faster by waiting"]]
[:h1 [link {:href url} "Faster by waiting"]]
[:div.demo-text
[:h2 "Reagent gets async rendering"]
@ -106,7 +108,7 @@
changes are rendered in one single go."]
(if summary
[link {:href (fvar main)
[link {:href url
:class 'news-read-more} "Read more"]
[:div.demo-text
@ -194,5 +196,4 @@
:reset-random-colors :color-choose :ncolors-choose
:palette :color-demo])}]])]]))
(swap! page-map assoc
"news/reagent-is-async.html" (fvar main))
(tools/register-page url (fn [] [main]))

View File

@ -1,12 +1,14 @@
(ns reagentdemo.news.clockpost
(:require [reagent.core :as r :refer [atom]]
[reagent.interop :refer-macros [.' .! fvar]]
[reagent.interop :refer-macros [.' .!]]
[reagent.debug :refer-macros [dbg]]
[reagentdemo.syntax :refer-macros [get-source]]
[reagentdemo.page :refer [title link page-map]]
[sitetools :as tools :refer [title link]]
[reagentdemo.common :as common :refer [demo-component]]
[reagentdemo.news.binaryclock :as binaryclock]))
(def url "news/binary-clock.html")
(def funmap (-> "reagentdemo/news/binaryclock.cljs"
get-source common/fun-map))
(def src-for (partial common/src-for funmap))
@ -24,7 +26,7 @@
clocksrc {:href "https://github.com/reagent-project/reagent/blob/master/demo/reagentdemo/news/binaryclock.cljs"}]
[:div.reagent-demo
[:h1 [link {:href (fvar main)} head]]
[:h1 [link {:href url} head]]
[title head]
[:div.demo-text
@ -42,8 +44,8 @@
[:p "So, without further ado, here is a binary clock using Reagent."]
(if summary
[link {:href (fvar main)
:class 'news-read-more} "Read more"]
[link {:href url
:class 'news-read-mode} "Read more"]
[:div.demo-text
[fn-src :nsr]
@ -119,5 +121,4 @@
description that corresponds to those arguments, and leave it
to React to actually display that UI."]])]]))
(swap! page-map assoc
"news/binary-clock.html" (fvar main))
(tools/register-page url (fn [] [main]))

View File

@ -1,12 +1,14 @@
(ns reagentdemo.news.undodemo
(:require [reagent.core :as reagent :refer [atom]]
[reagent.interop :refer-macros [.' .! fvar]]
[reagent.interop :refer-macros [.' .!]]
[reagent.debug :refer-macros [dbg println]]
[reagentdemo.syntax :refer-macros [get-source]]
[reagentdemo.page :refer [title link page-map]]
[sitetools :as tools :refer [title link]]
[reagentdemo.common :as common :refer [demo-component]]
[todomvc :as todomvc]))
(def url "news/cloact-reagent-undo-demo.html")
(def funmap (-> ::this get-source common/fun-map))
(def src-for (partial common/src-for funmap))
@ -48,7 +50,7 @@
(defn main [{:keys [summary]}]
(let [head "Cloact becomes Reagent: Undo is trivial"]
[:div.reagent-demo
[:h1 [link {:href (fvar main)} head]]
[:h1 [link {:href url} head]]
[title head]
[:div.demo-text
[:h2 "(reset! cloact-name \"Reagent\")"]
@ -65,7 +67,7 @@
search-and-replace should suffice."]
(if summary
[link {:href (fvar main)
[link {:href url
:class 'news-read-more} "Read more"]
[:div.demo-text
@ -86,5 +88,4 @@
[undo-demo-cleanup]])]]))
(swap! page-map assoc
"news/cloact-reagent-undo-demo.html" (fvar main))
(tools/register-page url (fn [] [main]))

View File

@ -1,96 +0,0 @@
(ns reagentdemo.page
(:require [reagent.core :as reagent :refer [atom partial]]
[reagent.interop :refer-macros [.' .! fvar fvar?]]
[reagent.debug :refer-macros [dbg]]
[clojure.string :as string]
[goog.events :as events]
[goog.history.EventType :as hevt])
(:import [goog History]
[goog.history Html5History]))
(def page (atom ""))
(def base-path (atom nil))
(def html5-history false)
(defn create-history []
(when reagent/is-client
(let [proto (-> js/window .-location .-protocol)]
(if (and (.isSupported Html5History)
(case proto "http:" true "https:" true false))
(do (set! html5-history true)
(doto (Html5History.)
(.setUseFragment false)))
(History.)))))
(def history (create-history))
(defn setup-history []
(when-let [h history]
(events/listen h hevt/NAVIGATE
(fn [e]
(reset! page (subs (.-token e)
(count @base-path)))
(reagent/flush)))
(add-watch page ::history (fn [_ _ oldp newp]
(when-not (= oldp newp)
(.setToken h (str @base-path newp)))))
(.setEnabled h true)))
(js/setTimeout setup-history 100)
(defn set-start-page [p]
(when html5-history
;; Find base-path for html5 history
(let [loc (-> js/window .-location .-pathname)
split #".[^/]*"
loc-parts (re-seq split loc)
page-parts (re-seq split (case p "" "." p))
base (str (apply str
(drop-last (count page-parts) loc-parts))
"/")]
(reset! base-path (string/replace base #"^/" ""))))
(reset! page p))
(def title-atom (atom ""))
(def page-map (atom nil))
(def reverse-page-map (atom nil))
(add-watch page-map ::page-map-watch
(fn [_ _ _ new-map]
(reset! reverse-page-map
(into {} (for [[k v] new-map]
[v k])))))
(defn prefix [href]
(let [depth (-> #"/" (re-seq @page) count)]
(str (->> "../" (repeat depth) (apply str)) href)))
(defn link [props child]
(let [rpm @reverse-page-map
href (-> props :href rpm)]
(assert (string? href))
[:a (assoc props
:href (prefix href)
:on-click (if history
(fn [e]
(.preventDefault e)
(reset! page href)
(reagent/next-tick
#(set! (.-scrollTop (.-body js/document))
0)))
identity))
child]))
(add-watch page ::title-watch
(fn [_ _ _ p]
;; First title on a page wins
(reset! title-atom "")))
(defn title [name]
(when (= @title-atom "")
(if reagent/is-client
(set! (.-title js/document) name))
(reset! title-atom name))
[:div])

259
demo/sitetools.cljs Normal file
View File

@ -0,0 +1,259 @@
(ns sitetools
(:require [clojure.string :as string]
[goog.events :as evt]
[goog.history.EventType :as hevt]
[reagent.core :as reagent :refer [atom partial]]
[reagent.debug :refer-macros [dbg log dev?]]
[reagent.interop :as i :refer-macros [.' .!]])
(:import [goog History]
[goog.history Html5History]
[goog.net Jsonp]))
(when (exists? js/console)
(enable-console-print!))
(declare page-content)
(declare prefix)
;;; Configuration
(defonce config (atom {:page-map {"index.html"
(fn [] [:div "Empty"])}
:body (fn [] [:div (page-content)])
:site-dir "outsite/public"
:css-infiles ["site/public/css/main.css"]
:css-file "css/built.css"
:js-file "js/main.js"
:js-dir "js/out"
:default-title ""
:allow-html5-history false}))
(defonce page (atom "index.html"))
(defonce page-title (atom (:default-title @config)))
(defonce page-state (atom {:has-history false}))
(defn register-page [pageurl comp]
(assert (string? pageurl) (str "expected string, not " pageurl))
(assert (fn? comp))
(swap! config update-in [:page-map] assoc pageurl comp))
;;; Components
(defn link
[props child]
(let [p (:href props)
f ((:page-map @config) p)]
(assert (ifn? f) (str "couldn't resolve ppage " p))
(assert (string? p))
[:a (assoc props
:href (prefix p)
:on-click (if (:has-history @page-state)
(fn [e]
(.preventDefault e)
(reset! page p)
(reagent/next-tick
#(set! (.-scrollTop (.-body js/document))
0)))
identity))
child]))
(defn title [name]
(when (= @page-title "")
;; First title on a page wins
(reset! page-title name)
(when reagent/is-client
(set! (.-title js/document) @page-title)))
nil)
(defn page-content []
[(get-in @config [:page-map @page]
(get-in @config [:page-map "index.html"]))])
;;; Implementation:
(defn default-content []
[:div "Empty"])
(add-watch page ::title-watch
(fn [_ _ _ p]
;; First title on a page wins
(reset! page-title "")))
;;; History
(defn use-html5-history []
(when reagent/is-client
(let [proto (.' js/window :location.protocol)]
(and (:allow-html5-history @config)
(.isSupported Html5History)
(#{"http:" "https:"} proto)))))
(defn create-history []
(if (use-html5-history)
(doto (Html5History.)
(.setUseFragment false))
(History.)))
(def history nil)
(defn token-base []
(if (use-html5-history)
(:base-path @config)))
(defn setup-history []
(when (nil? history)
(set! history (create-history))
(swap! page-state assoc :has-history (some? history))
(when-let [h history]
(evt/listen h hevt/NAVIGATE
(fn [e]
(let [t (.-token e)
bp (token-base)]
(reset! page (if (and bp (== 0 (.indexOf t bp)))
(subs t (count bp))
t)))
(reagent/flush)))
(add-watch page ::history
(fn [_ _ oldp newp]
(when-not (= oldp newp)
(.setToken h (str (token-base) newp)))))
(.setEnabled h true))))
(defn base-path [loc p]
;; Find base-path for html5 history
(let [split #".[^/]*"
depth (->> (case p "" "." p) (re-seq split) count)
base (->> loc (re-seq split) (drop-last depth) (apply str))]
(string/replace (str base "/") #"^/" "")))
(defn set-start-page [p]
(when (and (not (:base-path @config))
(use-html5-history))
(swap! config assoc :base-path
(base-path (.' js/window -location.pathname) p)))
(reset! page p))
(defn prefix [href]
(let [depth (-> #"/" (re-seq @page) count)]
(str (->> "../" (repeat depth) (apply str)) href)))
;;; Static site generation
(defn body []
(let [b (:body @config)]
(assert (fn? b))
[b]))
(defn danger [t s]
[t {:dangerouslySetInnerHTML {:__html s}}])
(defn html-template [{:keys [title body timestamp page-conf
opt-none req]}]
(let [c @config
base (prefix (str (:js-dir c) "/goog/base.js"))
main (str (prefix (:js-file c)) timestamp)
css-file (prefix (:css-file c))
opt-none (:opt-none c)]
(reagent/render-to-static-markup
[:html
[:head
[:meta {:charset "utf-8"}]
[:meta {:name 'viewport
:content "width=device-width, initial-scale=1.0"}]
[:link {:href (str css-file timestamp) :rel 'stylesheet}]
[:title title]]
[:body
(danger :div body)
(danger :script (str "var pageConfig = " (-> page-conf
clj->js
js/JSON.stringify)))
(if opt-none
[:script {:src base :type "text/javascript"}])
[:script {:src main :type "text/javascript"}]
(if opt-none
(danger :script "goog.require('devsetup');"))]])))
(defn gen-page [page-name timestamp]
(reset! page page-name)
(let [b (reagent/render-component-to-string (body))]
(str "<!doctype html>"
(html-template {:title @page-title
:body b
:page-conf {:allow-html5-history true
:page-name page-name}
:timestamp timestamp}))))
(defn mkdirs [f]
(let [fs (js/require "fs")
path (js/require "path")
items (as-> f _
(.' path dirname _)
(.' path normalize _)
(string/split _ #"/"))
parts (reductions #(str %1 "/" %2) items)]
(doseq [d parts]
(when-not (.' fs existsSync d)
(.' fs mkdirSync d)))))
(defn write-file [f content]
(let [fs (js/require "fs")]
(mkdirs f)
(.' fs writeFileSync f content)))
(defn read-file [f]
(let [fs (js/require "fs")]
(.' fs readFileSync f)))
(defn path-join [& paths]
(let [path (js/require "path")]
(apply (.' path :join) paths)))
(defn read-css []
(clojure.string/join "\\n"
(map read-file (:css-infiles @config))))
(defn write-resources [dir]
(write-file (path-join dir (:css-file @config))
(read-css)))
;;; Main entry points
(defn ^:export genpages [opts]
(log "Generating site")
(swap! config merge (js->clj opts :keywordize-keys true))
(let [dir (:site-dir @config)
written (atom #{})
timestamp (str "?" (.' js/Date now))
one-page (fn [] (first (filter
(fn [x] (nil? (@written x)))
(keys (:page-map @config)))))]
(loop [f (one-page)]
(when f
(swap! written conj f)
(write-file (path-join dir f)
(gen-page f timestamp))
(recur (one-page))))
(write-resources dir))
(log "Wrote site"))
(defn start! [site-config]
(swap! config merge site-config)
(when reagent/is-client
(let [conf (when (exists? js/pageConfig)
(js->clj js/pageConfig :keywordize-keys true))
page-name (:page-name conf)]
(when page-name
(set-start-page page-name))
(swap! config merge conf)
(setup-history)
(reagent/render-component (body)
(.' js/document :body)))))

22
env/dev/devsetup.cljs vendored Normal file
View File

@ -0,0 +1,22 @@
(ns devsetup
(:require
[demo :as site]
[runtests]
[reagent.core :as r]
[figwheel.client :as fw :include-macros true]))
(defn test! []
(runtests/run-tests))
(defn on-update []
(r/force-update-all)
(test!))
(when r/is-client
(fw/watch-and-reload
:websocket-url "ws://localhost:3449/figwheel-ws"
:jsload-callback #(on-update)))
(demo/start! {:test-results (fn []
[runtests/test-output-mini])})
(test!)

8
env/prod/prodsetup.cljs vendored Normal file
View File

@ -0,0 +1,8 @@
(ns envsetup
(:require [mysite]))
(mysite/start!)
(when
(exists? js/runtests)
(js/runtests.main))

View File

@ -1,3 +1,4 @@
@charset "utf-8";
html,
body {
margin: 0;

0
outsite/public/.keep Normal file
View File

View File

@ -3,39 +3,97 @@
:url "http://github.com/reagent-project/reagent"
:license {:name "MIT"}
:description "A simple ClojureScript interface to React"
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/clojurescript "0.0-2342"]]
:plugins [[lein-cljsbuild "1.0.3"]
[com.cemerick/clojurescript.test "0.3.1"]]
:profiles {:dev {:source-paths ["src" "demo"]}
:prod {:cljsbuild
{:builds
{:client {:compiler
{:optimizations :advanced
:elide-asserts true
:pretty-print false}}}}}
:test {:cljsbuild
{:builds
{:client {:source-paths ^:replace
["test" "src" "demo"
"examples/todomvc/src"
"examples/simple/src"
"examples/geometry/src"]}}}}
:srcmap {:cljsbuild
{:builds
{:client
{:compiler
{:source-map "target/cljs-client.js.map"
:source-map-path "client"}}}}}}
:source-paths ["src"]
:plugins [[lein-cljsbuild "1.0.3"]]
:resource-paths ["vendor"]
:cljsbuild
{:builds
{:client {:source-paths ["src" "demo" "examples/todomvc/src"
"examples/simple/src"
"examples/geometry/src"]
:notify-command ["node" "./bin/gen-site.js"]
:compiler
{:output-dir "target/client"
:output-to "target/cljs-client.js"
:pretty-print true}}}})
:source-paths ["src"]
:profiles {:dev-base {:dependencies
[[figwheel "0.1.5-SNAPSHOT"]]
:plugins [[lein-figwheel "0.1.5-SNAPSHOT"]]
:resource-paths ["site" "outsite"]
:figwheel {:css-dirs ["site/public/css"]}
:cljsbuild {:builds
{:client
{:source-paths ["env/dev"]
:compiler {:source-map true
:optimizations :none
:output-dir
"outsite/public/js/out"}}}}}
:site {:resource-paths ^:replace ["outsite"]
:figwheel {:css-dirs ^:replace ["outsite/public/css"]}
:cljsbuild {:builds
{:client
{:notify-command
["node" "bin/gen-site.js"]}}}}
:prod [:base :site
{:cljsbuild {:builds
{:client
{:source-paths ["env/prod"]
:compiler {:optimizations :advanced
:elide-asserts true
:output-dir "target/client"}}}}}]
:test {:dependencies [[com.cemerick/clojurescript.test "0.3.1"]]
:cljsbuild {:builds
{:client {:source-paths ["test"]}}}}
:dev [:dev-base :test]
:prod-test [:prod :test]}
:clean-targets ^{:protect false} [:target-path :compile-path
"outsite/public/js"
"outsite/public/site"
"outsite/public/news"
"outsite/public/index.html"
"out"]
:cljsbuild {:builds
{:client {:source-paths ["src"
"demo"
"examples/todomvc/src"
"examples/simple/src"
"examples/geometry/src"]
:compiler
{:output-to "outsite/public/js/main.js"}}}}
:figwheel {:http-server-root "public" ;; assumes "resources"
:server-port 3449}
;-------------------
;; :profiles {:dev {:source-paths ["src" "demo"]}
;; :prod {:cljsbuild
;; {:builds
;; {:client {:compiler
;; {:optimizations :advanced
;; :elide-asserts true
;; :pretty-print false}}}}}
;; :test {:cljsbuild
;; {:builds
;; {:client {:source-paths ^:replace
;; ["test" "src" "demo"
;; "examples/todomvc/src"
;; "examples/simple/src"
;; "examples/geometry/src"]}}}}
;; :srcmap {:cljsbuild
;; {:builds
;; {:client
;; {:compiler
;; {:source-map "target/cljs-client.js.map"
;; :source-map-path "client"}}}}}}
;; :cljsbuild
;; {:builds
;; {:client {:source-paths ["src" "demo" "examples/todomvc/src"
;; "examples/simple/src"
;; "examples/geometry/src"]
;; :notify-command ["node" "./bin/gen-site.js"]
;; :compiler
;; {:output-dir "target/client"
;; :output-to "target/cljs-client.js"
;; :pretty-print true}}}}
)

View File

@ -0,0 +1,579 @@
@charset "utf-8";
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea;
/* background: #eaeaea url('bg.png'); */
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
button,
input[type="checkbox"] {
outline: none;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input::-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
/* Mobile Safari */
border: none;
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
/* Mobile Safari */
border: none;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-ms-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden {
display: none;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
}
.learn-bar #todoapp {
width: 550px;
margin: 130px auto 40px auto;
}
}
div, h1, input {
font-family: HelveticaNeue, Helvetica;
color: #777;
}
.example-clock {
font-size: 128px;
line-height: 1.2em;
font-family: HelveticaNeue-UltraLight, Helvetica;
}
@media (max-width: 768px) {
.example-clock {
font-size: 64px;
}
}
.color-input, .color-input input {
font-size: 24px;
line-height: 1.5em;
}

View File

@ -1,24 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Testing reagent</title>
<link rel="stylesheet" href="../examples/todomvc/todos.css">
<link rel="stylesheet" href="../examples/todomvc/todosanim.css">
<link rel="stylesheet" href="../examples/simple/example.css">
<link rel="stylesheet" href="demo.css">
<style type="text/css">
.runtests { margin-bottom: 400px; }
</style>
</head>
<body>
<h1>This will become an example when compiled</h1>
<script type="text/javascript" src="../target/cljs-client.js"></script>
<script type="text/javascript">
if (typeof runtests !== 'undefined') {
runtests.mounttests();
} else {
demo.mountdemo();
}
</script>
</body>
</html>

View File

@ -1,18 +1,26 @@
(ns runtests
(:require [reagent.core :as reagent :refer [atom]]
[reagent.interop :refer-macros [.' .! fvar]]
[reagent.interop :refer-macros [.' .!]]
[reagent.debug :refer-macros [dbg println]]
[demo :as demo]
[cemerick.cljs.test :as t]))
[cemerick.cljs.test :as t]
[testreagent]
[testcursor]
[testinterop]
[testratom]))
(enable-console-print!)
(def test-results (atom nil))
(def test-box {:position 'absolute
:margin-left -35
:color :#aaa})
(defn test-output []
(let [res @test-results]
[:div {:style {:margin-top "40px"}}
[:div {:style test-box}
(if-not res
[:div "waiting for tests to run"]
[:div
@ -25,9 +33,10 @@
(let [res @test-results]
(if res
(if (zero? (+ (:fail res) (:error res)))
[:div "Tests ok"]
[:div {:style test-box}
"All tests ok"]
[test-output])
[:div "."])))
[:div {:style test-box} "testing"])))
(defn test-demo []
[:div
@ -48,8 +57,9 @@
(reset! test-results {:error e}))))
(println "-----------------------------------------"))
(if reagent/is-client
(do
(reset! test-results nil)
(js/setTimeout run-all-tests 1000))
(run-all-tests))
(defn run-tests []
(if reagent/is-client
(do
(reset! test-results nil)
(js/setTimeout run-all-tests 100))
(run-all-tests)))

View File

@ -1,18 +0,0 @@
(ns simpletest
(:require-macros [cemerick.cljs.test
:refer (is deftest with-test run-tests testing)])
(:require [cemerick.cljs.test :as t]))
;; (deftest somewhat-less-wat
;; (is (= "{}[]" (+ {} []))))
(deftest javascript-allows-div0
(is (= js/Infinity (/ 1 0) (/ (int 1) (int 0)))))
;; (with-test
;; (defn pennies->dollar-string
;; [pennies]
;; {:pre [(integer? pennies)]}
;; (str "$" (int (/ pennies 100)) "." (mod pennies 100)))
;; (testing "assertions are nice"
;; (is (thrown-with-msg? js/Error #"integer?" (pennies->dollar-string 564.2)))))