diff --git a/.babelrc b/.babelrc
new file mode 100644
index 000000000..b0b9a96ef
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "stage": 0
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..c1b55337d
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+# editorconfig.org
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+
+[*.md]
+# Allow
from Markdown
+trim_trailing_whitespace = false
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 000000000..cc22cddb9
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,78 @@
+---
+root: true
+extends: eslint:recommended
+
+# babel-eslint support more syntax stuff than eslint for now
+parser: babel-eslint
+
+ecmaFeatures:
+ modules: true
+ jsx: true
+
+env:
+ es6: true
+ browser: true
+ node: true
+
+globals:
+ __DEV__: true
+ __DEVTOOLS__: true
+ __DEVSERVER__: true
+ __PROD__: true
+ __SERVER_PROTOCOL__: true
+ __SERVER_HOSTNAME__: true
+ __SERVER_PORT__: true
+ __SERVER_HOST__: true
+ __SERVER_URL__: true
+
+plugins:
+ - react
+
+# 0: off, 1: warning, 2: error
+rules:
+ no-console: 0
+
+ indent: [2, 2] # 2 spaces indentation
+ max-len: [2, 80, 4]
+ quotes: [2, "double"]
+ semi: [2, "never"]
+ no-multiple-empty-lines: [2, {"max": 1}]
+
+ brace-style: [2, "stroustrup"]
+ comma-dangle: [2, "always-multiline"]
+ comma-style: [2, "last"]
+ dot-location: [2, "property"]
+
+ one-var: [2, "never"]
+ no-var: [2]
+ prefer-const: [2]
+ no-bitwise: [2]
+
+ object-curly-spacing: [2, "always"]
+ array-bracket-spacing: [2, "always"]
+ #computed-property-spacing: [2, "always"]
+
+ space-unary-ops: [2, {"words": true, "nonwords": false}]
+ space-after-keywords: [2, "always"]
+ space-before-blocks: [2, "always"]
+ space-before-function-paren: [2, "never"]
+ space-in-parens: [2, "never"]
+ spaced-comment: [2, "always"]
+
+ # eslint-plugin-react rules
+ react/jsx-boolean-value: 2
+ react/jsx-no-undef: 2
+ react/jsx-quotes: 2
+ #react/jsx-sort-prop-types: 2
+ #react/jsx-sort-props: 2
+ react/jsx-uses-react: 2
+ react/jsx-uses-vars: 2
+ react/no-did-mount-set-state: 2
+ react/no-did-update-set-state: 2
+ react/no-multi-comp: 2
+ react/no-unknown-property: 2
+ react/prop-types: 2
+ react/react-in-jsx-scope: 2
+ react/self-closing-comp: 2
+ react/sort-comp: 2
+ react/wrap-multilines: 2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..08c1973c2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+# build
+lib
+
+# demo build
+dist
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..68b5c45d8
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: nodejs
+nodejs:
+ - iojs
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..8efb76fba
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 0.0.0
+
+No release has been made yet. Everything is highly subject to changes.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..6df92a4ae
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Maxime Thirouin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..764088f49
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+# statinamic
+
+> A static website generator that create dynamic website using React components.
+
+![Travis Build Badge](https://img.shields.io/travis/MoOx/statinamic/master.svg)
+
+## Install
+
+```console
+$ npm install MoOx/statinamic
+```
+
+## Usage
+
+See [demo](demo) for now.
+
+```console
+$ npm install
+
+# static build
+$ npm test
+
+# live demo
+$ npm run demo
+```
+
+---
+
+## Contributing
+
+* ⇄ Pull requests and ★ stars are always welcome.
+* For bugs and feature requests, please create an issue.
+* Pull requests that modifies code must be accompanied with automated tests.
+* Run `$ npm test` before making a pull request.
+
+## [CHANGELOG](CHANGELOG.md)
+
+## [LICENSE](LICENSE)
diff --git a/demo/build.js b/demo/build.js
new file mode 100644
index 000000000..2d384c94e
--- /dev/null
+++ b/demo/build.js
@@ -0,0 +1,168 @@
+import path from "path"
+import webpack from "webpack"
+import ExtractTextPlugin from "extract-text-webpack-plugin"
+
+import markdownIt from "markdown-it"
+import markdownItTocAndAnchor from "markdown-it-toc-and-anchor"
+import hljs from "highlight.js"
+
+import pkg from "./package.json"
+
+import routes from "app/routes"
+// import * as reducers from "app/ducks"
+import * as pageComponents from "app/pageComponents"
+// instead of using the collection.json that has been made in during build
+// we directly use the module cache responsible of the build, since it's still
+// in memory. This avoid us a fs read + handling some potential async issues
+// (since the collection.json is made by a plugin _after_ the build)
+import collection from "statinamic/lib/json-collection-loader/cache"
+
+import build from "statinamic/lib/build"
+import configurator from "statinamic/lib/configurator"
+import jsonCollectionPlugin from
+ "statinamic/lib/json-collection-loader/plugin"
+
+const config = configurator(pkg)
+
+const root = path.join(__dirname)
+const source = path.join(root, "content")
+const dest = path.join(root, "dist")
+
+build({
+ config,
+ source,
+ dest,
+
+ exports: {
+ routes,
+ initialState: {
+ pageComponents,
+ collection,
+ },
+ },
+
+ webpack: {
+ entry: {
+ index: [
+ path.join(__dirname, "client"),
+ ],
+ },
+
+ output: {
+ path: dest,
+ filename: "[name].js",
+ publicPath: "/",
+ },
+
+ resolve: {
+ extensions: [
+ // node default extensions
+ ".js",
+ ".json",
+ // for all other extensions specified directly
+ "",
+ ],
+ },
+
+ module: {
+ // ! \\ note that loaders are executed from bottom to top !
+ loaders: [
+ //
+ // statinamic requirement
+ //
+ {
+ test: /\.md$/,
+ loaders: [
+ `file?name=[path][name]/index.json&context=${ source }`,
+ "statinamic/lib/json-collection-loader",
+ "statinamic/lib/markdown-as-json-loader",
+ ],
+ },
+ {
+ test: /\.json$/,
+ loaders: [
+ "json",
+ ],
+ },
+
+ // your loaders
+ {
+ test: /\.js$/,
+ loaders: [
+ ...config.__DEV__ && [ "react-hot" ],
+ "babel",
+ ...config.__DEV__ && [ "eslint" ],
+ ],
+ exclude: /node_modules/,
+ },
+ ],
+ },
+
+ plugins: [
+ //
+ // statinamic requirement
+ //
+ jsonCollectionPlugin({
+ filename: "collection.json",
+ }),
+
+ // your plugins
+ new webpack.DefinePlugin(
+ // transform string as "string" so hardcoded replacements are
+ // syntaxically correct
+ Object.keys(config).reduce((obj, constName) => {
+ const value = config[constName]
+ return {
+ ...obj,
+ [constName]: (
+ typeof value === "string" ? JSON.stringify(value) : value
+ ),
+ }
+ }, {})
+ ),
+ new ExtractTextPlugin("[name].css", { disable: !config.__PROD__ }),
+ ...config.__PROD__ && [
+ new webpack.optimize.DedupePlugin(),
+ new webpack.optimize.UglifyJsPlugin({
+ compress: {
+ warnings: false,
+ },
+ }),
+ ],
+ ],
+
+ node: {
+ // https://github.com/webpack/webpack/issues/451
+ // run tape test with webpack
+ fs: "empty",
+ },
+
+ markdownIt: (
+ markdownIt({
+ html: true,
+ linkify: true,
+ typographer: true,
+ highlight: (code, lang) => {
+ code = code.trim()
+ // language is recognized by highlight.js
+ if (lang && hljs.getLanguage(lang)) {
+ return hljs.highlight(lang, code).value
+ }
+ // ...or fallback to auto
+ return hljs.highlightAuto(code).value
+ },
+ })
+ .use(markdownItTocAndAnchor, {
+ tocFirstLevel: 2,
+ })
+ ),
+
+ jsonCollection: {
+ urlify: (url) => url
+ // .replace(/^content\//, "")
+ .replace(/^demo\/content\//, "")
+ .replace(/\.md$/, "")
+ ,
+ },
+ },
+})
diff --git a/demo/client.js b/demo/client.js
new file mode 100644
index 000000000..fa1435cdc
--- /dev/null
+++ b/demo/client.js
@@ -0,0 +1,22 @@
+import statinamic from "statinamic/lib/client"
+
+import routes from "app/routes"
+// import * as reducers from "app/ducks"
+import * as pageComponents from "app/pageComponents"
+
+// dev index
+if (__DEV__) {
+ require("!!file?name=index.html!statinamic/lib/dev-index.html")
+}
+
+// all md files as JSON + generate collections
+require.context("./content", true, /\.md$/)
+
+statinamic({
+ routes,
+ // reducers,
+ initialState: {
+ ...window.__INITIAL_STATE__,
+ pageComponents,
+ },
+})
diff --git a/demo/content/blog.md b/demo/content/blog.md
new file mode 100644
index 000000000..24ee1e0e4
--- /dev/null
+++ b/demo/content/blog.md
@@ -0,0 +1,6 @@
+---
+title: Blog
+layout: Collection
+---
+
+Here is a list of posts. Below are other pages.
diff --git a/demo/content/blog/react.md b/demo/content/blog/react.md
new file mode 100644
index 000000000..30d89b8b7
--- /dev/null
+++ b/demo/content/blog/react.md
@@ -0,0 +1,7 @@
+---
+title: React blog post
+layout: Post
+date: 2015-08-20
+---
+
+That's cool yo.
diff --git a/demo/content/blog/test.md b/demo/content/blog/test.md
new file mode 100644
index 000000000..072e13c6b
--- /dev/null
+++ b/demo/content/blog/test.md
@@ -0,0 +1,11 @@
+---
+title: TeStInG
+layout: Post
+date: 2015-01-02
+---
+
+This is a **post**.
+
+## Hell yeah
+
+You feel the nice platform, do you?
diff --git a/demo/content/contact.md b/demo/content/contact.md
new file mode 100644
index 000000000..385e06afe
--- /dev/null
+++ b/demo/content/contact.md
@@ -0,0 +1,5 @@
+---
+title: Contact
+---
+
+Mail yo.
diff --git a/demo/package.json b/demo/package.json
new file mode 100644
index 000000000..b53450b9b
--- /dev/null
+++ b/demo/package.json
@@ -0,0 +1,17 @@
+{
+ "private": true,
+ "name": "statinamic-demo",
+ "url": "http://moox.io/statinamic",
+ "version": "0.0.0",
+ "author": "Maxime Thirouin",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/MoOx/statinamic.git"
+ },
+ "statinamic": {
+ "hostname": "moox.io"
+ },
+ "twitter": "MoOx",
+ "github": "MoOx"
+}
diff --git a/demo/web_modules/Collection/index.js b/demo/web_modules/Collection/index.js
new file mode 100644
index 000000000..fd44d842f
--- /dev/null
+++ b/demo/web_modules/Collection/index.js
@@ -0,0 +1,78 @@
+import React, { Component } from "react"
+import { PropTypes } from "react"
+
+import Page from "Page"
+
+import { connect } from "react-redux"
+import enhanceCollection from "statinamic/lib/enhance-collection"
+
+export default
+@connect(
+ ({ collection }) => {
+ return { collection }
+ }
+)
+class Collection extends Component {
+
+ static propTypes = {
+ head: PropTypes.object.isRequired,
+ body: PropTypes.string.isRequired,
+ collection: PropTypes.array,
+ }
+
+ render() {
+ const {
+ collection,
+ } = this.props
+ return (
+
+
+ {
+ collection && collection.length &&
+
+
+ { "Posts (by date)" }
+
+ {
+ enhanceCollection(collection, {
+ filter: { layout: "Post" },
+ sort: "date",
+ reverse: true,
+ // limit: 1,
+ })
+ .map((item) => {
+ return (
+ -
+
+ { item.title }
+
+
+ )
+ })
+ }
+
+
+ { "Other pages" }
+
+ {
+ enhanceCollection(collection, {
+ filter: ({ layout }) => layout !== "Post",
+ sort: "title",
+ })
+ .map((item) => {
+ return (
+ -
+
+ { item.title }
+
+
+ )
+ })
+ }
+
+
+ }
+
+ )
+ }
+}
diff --git a/demo/web_modules/Home/index.js b/demo/web_modules/Home/index.js
new file mode 100644
index 000000000..c75b1b1c4
--- /dev/null
+++ b/demo/web_modules/Home/index.js
@@ -0,0 +1,17 @@
+import React, { Component } from "react"
+// import { PropTypes } from "react"
+
+export default class Home extends Component {
+
+ // static propTypes = {
+ // title: PropTypes.bool,
+ // }
+
+ render() {
+ return (
+
+ Home !
+
+ )
+ }
+}
diff --git a/demo/web_modules/Layout/index.js b/demo/web_modules/Layout/index.js
new file mode 100644
index 000000000..b3fc8cf03
--- /dev/null
+++ b/demo/web_modules/Layout/index.js
@@ -0,0 +1,23 @@
+import React, { Component } from "react"
+import { PropTypes } from "react"
+import { Link } from "react-router"
+
+export default class Layout extends Component {
+
+ static propTypes = {
+ children: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]),
+ }
+
+ render() {
+ return (
+
+
+ {this.props.children}
+
+ )
+ }
+}
diff --git a/demo/web_modules/NotFound/index.js b/demo/web_modules/NotFound/index.js
new file mode 100644
index 000000000..5dc0625fd
--- /dev/null
+++ b/demo/web_modules/NotFound/index.js
@@ -0,0 +1,13 @@
+import React, { Component } from "react"
+// import { PropTypes } from "react"
+
+export default class NotFound extends Component {
+
+ render() {
+ return (
+
+ 404 Not Found
+
+ )
+ }
+}
diff --git a/demo/web_modules/Page/index.js b/demo/web_modules/Page/index.js
new file mode 100644
index 000000000..187bf01fd
--- /dev/null
+++ b/demo/web_modules/Page/index.js
@@ -0,0 +1,57 @@
+import React, { Component } from "react"
+import { PropTypes } from "react"
+import Helmet from "react-helmet"
+
+import pkg from "../../package.json"
+
+// function pageDescription(text) {
+// return text
+// }
+
+export default class Page extends Component {
+
+ static propTypes = {
+ children: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]),
+ head: PropTypes.object.isRequired,
+ body: PropTypes.string.isRequired,
+ }
+
+ render() {
+ const {
+ head,
+ body,
+ } = this.props
+
+ const meta = [
+ { name: "twitter:title", content: head.title },
+ { name: "twitter:creator", content: `@${ pkg.twitter }` },
+ // { name: "twitter:site", content: `@${ pkg.twitter }` },
+ // { name: "twitter:description", content: pageDescription(body) },
+ // { name: "twitter:image", content: header.image },
+ { property: "og:site_name", content: pkg.name },
+ { property: "og:title", content: head.title },
+ { property: "og:type", content: "article" },
+ // { property: "og:url", content: "http://www.example.com/" },
+ // { property: "og:description", content: pageDescription(body) },
+ // { property: "og:image", content: header.image },
+ ]
+
+ return (
+
+
+
+
{ head.title }
+ {
+ body &&
+
+ }
+ { this.props.children }
+
+ )
+ }
+}
diff --git a/demo/web_modules/Post/index.js b/demo/web_modules/Post/index.js
new file mode 100644
index 000000000..189fc3481
--- /dev/null
+++ b/demo/web_modules/Post/index.js
@@ -0,0 +1,21 @@
+import React, { Component } from "react"
+import { PropTypes } from "react"
+
+import Page from "Page"
+
+export default class Post extends Component {
+
+ static propTypes = {
+ head: PropTypes.object.isRequired,
+ body: PropTypes.string.isRequired,
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/demo/web_modules/app/pageComponents.js b/demo/web_modules/app/pageComponents.js
new file mode 100644
index 000000000..71f0c2609
--- /dev/null
+++ b/demo/web_modules/app/pageComponents.js
@@ -0,0 +1,3 @@
+export Page from "Page"
+export Post from "Post"
+export Collection from "Collection"
diff --git a/demo/web_modules/app/routes.js b/demo/web_modules/app/routes.js
new file mode 100644
index 000000000..260053674
--- /dev/null
+++ b/demo/web_modules/app/routes.js
@@ -0,0 +1,16 @@
+import React from "react"
+import { Route } from "react-router"
+// import PageContainer from "statinamic/lib/PageContainer"
+import PageContainer from "../../../src/PageContainer"
+
+// components
+import Layout from "Layout"
+import Home from "Home"
+
+// routes
+export default (
+
+
+
+
+)
diff --git a/demo/web_modules/statinamic/THIS_FOLDER_IS_A_HACK_FOR_DEMO_ONLY b/demo/web_modules/statinamic/THIS_FOLDER_IS_A_HACK_FOR_DEMO_ONLY
new file mode 100644
index 000000000..e69de29bb
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..628c38433
--- /dev/null
+++ b/package.json
@@ -0,0 +1,56 @@
+{
+ "private": true,
+ "name": "statinamic",
+ "version": "0.0.0",
+ "author": "Maxime Thirouin",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/MoOx/statinamic.git"
+ },
+ "dependencies": {
+ "chalk": "^1.1.0",
+ "eslint": "^1.1.0",
+ "eslint-plugin-react": "^3.2.2",
+ "gray-matter": "^2.0.0",
+ "loader-utils": "^0.2.11",
+ "markdown-it": "^4.4.0",
+ "mkdirp": "^0.5.1",
+ "nano-logger": "^1.0.0",
+ "opn": "^3.0.2",
+ "rimraf": "^2.4.2",
+ "webpack": "^1.11.0",
+ "webpack-dev-server": "^1.10.1",
+ "webpack-error-notification": "^0.1.4",
+ "webpack-nano-logs": "^1.0.0",
+ "whatwg-fetch": "^0.9.0"
+ },
+ "devDependencies": {
+ "babel": "^5.8.21",
+ "babel-core": "^5.8.22",
+ "babel-eslint": "^4.0.6",
+ "babel-loader": "^5.3.2",
+ "eslint-loader": "^1.0.0",
+ "extract-text-webpack-plugin": "^0.8.2",
+ "file-loader": "^0.8.1",
+ "json-loader": "^0.5.2",
+ "markdown-it-toc-and-anchor": "^1.0.1",
+ "react-hot-loader": "^1.1.4",
+ "highlight.js": "^8.5.0",
+ "react": "^0.13.3",
+ "react-helmet": "^1.1.1",
+ "react": "^0.13.3",
+ "react-redux": "^0.8.0",
+ "react-router": "1.0.0-beta3",
+ "redux": "^1.0.1",
+ "redux-devtools": "^1.0.2",
+ "redux-thunk": "^0.1.0"
+ },
+ "scripts": {
+ "prebabelify": "rimraf lib",
+ "babelify": "babel --copy-files src --out-dir lib",
+ "prepublish": "npm run babelify",
+ "test": "NODE_PATH=demo/web_modules babel-node demo/build --production",
+ "demo": "NODE_PATH=demo/web_modules babel-node demo/build --dev --dev-server --dev-tools --open"
+ }
+}
diff --git a/src/PageContainer/index.js b/src/PageContainer/index.js
new file mode 100644
index 000000000..f42a28a1e
--- /dev/null
+++ b/src/PageContainer/index.js
@@ -0,0 +1,106 @@
+import React, { Component } from "react"
+import { PropTypes } from "react"
+
+import { connect } from "react-redux"
+import * as pageActions from "../ducks/pages"
+
+export default
+@connect(
+ ({ pages, pageComponents }) => {
+ return { pages, pageComponents }
+ },
+ (dispatch) => {
+ return {
+ getPage: (page) => dispatch(pageActions.get(page)),
+ setPageType: (page, type) => dispatch(pageActions.setType(page, type)),
+ unknownPageType: (page, type) =>
+ dispatch(pageActions.unknownType(page, type)),
+ }
+ },
+)
+class PageContainer extends Component {
+
+ static propTypes = {
+ pages: PropTypes.object.isRequired,
+ pageComponents: PropTypes.object.isRequired,
+ params: PropTypes.object,
+
+ // components
+ NotFound: PropTypes.object,
+ Loading: PropTypes.object,
+
+ // actions
+ setPageType: PropTypes.func.isRequired,
+ unknownPageType: PropTypes.func.isRequired,
+ }
+
+ componentWillMount() {
+ this.preparePage(this.props)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.preparePage(nextProps)
+ }
+
+ preparePage(props) {
+ const pageKey = props.params.splat.replace(/\/index\.html$/, "")
+ const page = props.pages[pageKey]
+ if (!page) {
+ props.getPage(pageKey)
+ }
+ else {
+ if (page.error) {
+ return
+ }
+
+ if (!page.type) {
+ let pageComponentName = "Page"
+ if (page && page.head) {
+ pageComponentName = page.head.layout || page.head.type
+ }
+
+ const PageComponent = props.pageComponents[pageComponentName]
+ if (!PageComponent) {
+ props.unknownPageType(pageKey, pageComponentName)
+ }
+ }
+ }
+ }
+
+ render() {
+ // console.log(this.props)
+ const pageKey = this.props.params.splat
+
+ const page = this.props.pages[pageKey]
+ if (!page) {
+ // console.log("FUCK WAT")
+ return null
+ }
+
+ const PageComponent = this.props.pageComponents[page.type]
+
+ return (
+
+ {
+ page && !page.error && page.loading && this.props.Loading
+ }
+
+ {
+ page && !!page.error && !this.props.NotFound &&
+
+
{ page.error }
+
{ page.errorText }
+
+ }
+ {
+ page && !!page.error && this.props.NotFound
+ }
+
+ {
+ page && !page.error && !page.loading && PageComponent &&
+
+ }
+
+ )
+ }
+}
diff --git a/src/build/dev-server.js b/src/build/dev-server.js
new file mode 100644
index 000000000..2a537ea5b
--- /dev/null
+++ b/src/build/dev-server.js
@@ -0,0 +1,90 @@
+import webpack from "webpack"
+import WebpackDevServer from "webpack-dev-server"
+
+import webpackNanoLogs from "webpack-nano-logs"
+import WebpackErrorNotificationPlugin from "webpack-error-notification"
+
+import opn from "opn"
+import logger from "nano-logger"
+
+const log = logger("webpack-dev-server")
+
+export default (config, options) => {
+ options = {
+ protocol: "http://",
+ host: "0.0.0.0",
+ port: 3000,
+ open: true,
+ noDevEntriesTest: /^tests/,
+ ...(options || {}),
+ }
+
+ const serverUrl = `${ options.protocol }${ options.host }:${ options .port }`
+
+ const devEntries = [
+ `webpack-dev-server/client?${ serverUrl }`,
+ `webpack/hot/only-dev-server`,
+ ]
+
+ const devConfig = {
+ ...config,
+ debug: true,
+ watch: true,
+ colors: true,
+ progress: true,
+ entry: {
+ // add devEntries
+ ...Object.keys(config.entry)
+ .reduce(
+ (acc, key) => {
+ // some entries do not need extra stuff
+ acc[key] = key.match(options.noDevEntriesTest) !== null
+ ? config.entry[key]
+ : [
+ ...devEntries,
+ ...config.entry[key],
+ ]
+ return acc
+ },
+ {}
+ ),
+ },
+ plugins: [
+ ...(config.plugins || []),
+ ...(options.plugins || []),
+ new webpack.NoErrorsPlugin(),
+ new webpack.HotModuleReplacementPlugin(),
+ webpackNanoLogs,
+ new WebpackErrorNotificationPlugin(),
+ ],
+ eslint: {
+ ...config.eslint,
+ emitWarning: true,
+ },
+ }
+
+ return new WebpackDevServer(
+ webpack(devConfig),
+ {
+ https: options.protocol === "https://",
+ contentBase: config.output.path,
+ hot: true,
+ stats: {
+ colors: true,
+ // hide all chunk dependencies because it's unreadable
+ chunkModules: false,
+ // noize
+ assets: false,
+ },
+ noInfo: true,
+
+ // allow all url to point to index.html
+ historyApiFallback: true,
+ })
+ .listen(options.port, options.host, () => {
+ log(`Dev server started on ${ serverUrl }`)
+ if (options.open) {
+ opn(`${ serverUrl }`)
+ }
+ })
+}
diff --git a/src/build/index.js b/src/build/index.js
new file mode 100644
index 000000000..af8a5d9e6
--- /dev/null
+++ b/src/build/index.js
@@ -0,0 +1,84 @@
+import { sync as rm } from "rimraf"
+import { sync as mkdir } from "mkdirp"
+import color from "chalk"
+import nanoLogger from "nano-logger"
+
+import webpack from "./webpack"
+import devServer from "./dev-server"
+
+export default function(options) {
+ const {
+ config,
+ source,
+ } = options
+ const webpackConfig = options.webpack
+
+ const log = nanoLogger("statinamic/lib/build")
+
+ JSON.stringify(config, null, 2).split("\n").forEach(l => log(l))
+
+ const dest = webpackConfig.output.path
+
+ // cleanup
+ // rm(dest)
+ // mkdir(dest)
+
+ if (config.__DEVSERVER__) {
+ devServer(webpackConfig, {
+ protocol: config.__SERVER_PROTOCOL__,
+ host: config.__SERVER_HOSTNAME__,
+ port: config.__SERVER_PORT__,
+ open: process.argv.includes("--open"),
+ })
+ }
+ else {
+ webpack(webpackConfig, log, (stats) => {
+ log(color.green("✓ Static assets: build completed"))
+
+ // faking webpack.DefinePlugin for node usage
+ Object.keys(config).forEach((key) => {
+ global[key] = config[key]
+ })
+
+ const filenameLengthToSkip = source.length + 1
+ const extLengthToSkip = ".md".length
+
+ const pageUrls = stats.compilation.fileDependencies.reduce(
+ (array, filename) => {
+ if (filename.match(/\.md$/)) {
+ array.push(
+ filename
+ // remove ext
+ .slice(0, filename.length - extLengthToSkip)
+ // remove source
+ .substr(filenameLengthToSkip)
+ )
+ }
+ return array
+ },
+ []
+ )
+
+ require("../to-static-html")({
+ urls: [ "", ...pageUrls ],
+ source,
+ dest,
+ exports: options.exports,
+ log,
+ })
+ .then(
+ (files) => {
+ log(
+ color.green(`✓ Static html files: ${ files.length } files written`)
+ )
+ },
+ (error) => {
+ log(color.red(`✗ Static html files: failed to create files`))
+ setTimeout(() => {
+ throw error
+ }, 1)
+ }
+ )
+ })
+ }
+}
diff --git a/src/build/webpack.js b/src/build/webpack.js
new file mode 100644
index 000000000..9de629efd
--- /dev/null
+++ b/src/build/webpack.js
@@ -0,0 +1,30 @@
+import webpack from "webpack"
+import color from "chalk"
+
+export default (webpackConfig, log, cb) => {
+ webpack(webpackConfig, (err, stats) => {
+ if (err) {
+ throw err
+ }
+
+ if (stats.hasErrors()) {
+ stats.compilation.errors.forEach(
+ item => log(...[
+ color.red("Error:"),
+ ...item.message.split("\n"),
+ ])
+ )
+ throw new Error("webpack build failed with errors")
+ }
+ if (stats.hasWarnings()) {
+ stats.compilation.warnings.forEach(
+ item => log(...[
+ color.yellow("Warning:"),
+ ...item.message.split("\n"),
+ ])
+ )
+ }
+
+ cb(stats)
+ })
+}
diff --git a/src/client.js b/src/client.js
new file mode 100644
index 000000000..4fa711ba3
--- /dev/null
+++ b/src/client.js
@@ -0,0 +1,60 @@
+// App
+import "whatwg-fetch"
+
+import React from "react"
+
+import Router from "react-router"
+
+import { Provider } from "react-redux"
+import createStore from "./createStore"
+
+import fetchJSON from "./fetchJSON"
+
+export default function statinamic({
+ routes,
+ reducers = {},
+ initialState = {},
+}) {
+ const store = createStore(reducers, initialState)
+
+ // react-router beta4
+ // const history = require("history/lib/createBrowserHistory")()
+ const history = require("react-router/lib/BrowserHistory").history
+
+ let devtools = false
+ if (__DEVTOOLS__) {
+ const {
+ DevTools,
+ DebugPanel,
+ LogMonitor,
+ } = require("redux-devtools/lib/react")
+ devtools = (
+
+
+
+ )
+ }
+
+ fetchJSON(`/collection.json`)
+ .then(
+ ({ data }) => store.dispatch({
+ type: "COLLECTION_SET",
+ collection: data,
+ }),
+ (error) => store.dispatch({
+ type: "COLLECTION_ERROR",
+ error,
+ })
+ )
+
+ React.render(
+
+
+ {() => }
+
+ { __DEVTOOLS__ && devtools }
+
,
+ document.getElementById("statinamic")
+ )
+
+}
diff --git a/src/configurator.js b/src/configurator.js
new file mode 100644
index 000000000..5f016ace5
--- /dev/null
+++ b/src/configurator.js
@@ -0,0 +1,56 @@
+export default function config(pkg = {}) {
+ const production = process.argv.includes("--production")
+
+ if (production) {
+ if (!pkg.statinamic) {
+ throw new Error(
+ "Your package.json needs a 'statinamic' configuration section"
+ )
+ }
+ if (!pkg.statinamic.hostname) {
+ throw new Error(
+ "Your package.json/statinamic section require a 'hostname'"
+ )
+ }
+ }
+
+ // default
+ const statinamicConfig = {
+ // hostname: null,
+ dev: {
+ hostname: "0.0.0.0",
+ port: 3000,
+ },
+ ...pkg.statinamic || {},
+ }
+
+ const config = {
+ __DEV__: process.argv.includes("--dev"),
+ __DEVTOOLS__: process.argv.includes("--dev-tools"),
+ __DEVSERVER__: process.argv.includes("--dev-server"),
+ __PROD__: production,
+ __SERVER_PROTOCOL__: statinamicConfig.protocol || "http://",
+ ...(
+ production
+ ? {
+ "process.env": {
+ NODE_ENV: JSON.stringify("production"),
+ },
+ __SERVER_HOSTNAME__: statinamicConfig.hostname,
+ __SERVER_HOST__: statinamicConfig.hostname,
+ }
+ : {
+ __SERVER_HOSTNAME__: statinamicConfig.dev.hostname,
+ __SERVER_PORT__: statinamicConfig.dev.port,
+ __SERVER_HOST__:
+ statinamicConfig.dev.hostname +
+ ":" +
+ statinamicConfig.dev.port,
+ }
+ ),
+ }
+
+ config.__SERVER_URL__ = config.__SERVER_PROTOCOL__ + config.__SERVER_HOST__
+
+ return config
+}
diff --git a/src/createStore.js b/src/createStore.js
new file mode 100644
index 000000000..4a5b5b8b9
--- /dev/null
+++ b/src/createStore.js
@@ -0,0 +1,50 @@
+import { createStore, applyMiddleware, combineReducers, compose } from "redux"
+import thunk from "redux-thunk"
+
+import * as reducers from "./ducks"
+
+export default function(additionalReducers = {}, initialState = {}) {
+ const reducer = combineReducers({
+ ...reducers,
+ ...additionalReducers,
+ })
+
+ function promiseMiddleware() {
+ return (next) => (action) => {
+ const { promise, types, ...rest } = action
+ if (!promise) {
+ return next(action)
+ }
+ else if (!promise.then) {
+ throw new Error(
+ "promiseMiddleware expects a promise object that implements then()"
+ )
+ }
+
+ const [ REQUEST, SUCCESS, FAILURE ] = types
+ next({ ...rest, type: REQUEST })
+ return promise.then(
+ (response) => next({ ...rest, response, type: SUCCESS }),
+ (error) => next({ ...rest, error, type: FAILURE })
+ )
+ }
+ }
+
+ let finalCreateStore
+
+ if (__DEVTOOLS__) {
+ const { devTools, persistState } = require("redux-devtools")
+
+ finalCreateStore = compose(
+ applyMiddleware(promiseMiddleware, thunk),
+ devTools(),
+ persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)),
+ createStore
+ )
+ }
+ else {
+ finalCreateStore = applyMiddleware(promiseMiddleware, thunk)(createStore)
+ }
+
+ return finalCreateStore(reducer, initialState)
+}
diff --git a/src/dev-index.html b/src/dev-index.html
new file mode 100644
index 000000000..f82f30970
--- /dev/null
+++ b/src/dev-index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+ ...
+
+
+
diff --git a/src/ducks/README.md b/src/ducks/README.md
new file mode 100644
index 000000000..d4728bb2b
--- /dev/null
+++ b/src/ducks/README.md
@@ -0,0 +1 @@
+https://github.com/erikras/ducks-modular-redux
diff --git a/src/ducks/collection.js b/src/ducks/collection.js
new file mode 100644
index 000000000..6d79f13ab
--- /dev/null
+++ b/src/ducks/collection.js
@@ -0,0 +1,19 @@
+export default function reducer(state = [], action) {
+
+ switch (action.type) {
+ case "COLLECTION_GET":
+ return []
+
+ case "COLLECTION_SET":
+ return action.collection
+
+ case "COLLECTION_FORGET":
+ return []
+
+ case "COLLECTION_ERROR":
+ return []
+
+ default:
+ return state
+ }
+}
diff --git a/src/ducks/index.js b/src/ducks/index.js
new file mode 100644
index 000000000..91a7b0054
--- /dev/null
+++ b/src/ducks/index.js
@@ -0,0 +1,3 @@
+export collection from "./collection"
+export pageComponents from "./pageComponents"
+export pages from "./pages"
diff --git a/src/ducks/pageComponents.js b/src/ducks/pageComponents.js
new file mode 100644
index 000000000..640281988
--- /dev/null
+++ b/src/ducks/pageComponents.js
@@ -0,0 +1,11 @@
+export default function reducer(state = {}, action) {
+ switch (action.type) {
+ case "PAGE_COMPONENTS_SET":
+ return {
+ ...action.pageComponents,
+ }
+
+ default:
+ return state
+ }
+}
diff --git a/src/ducks/pages.js b/src/ducks/pages.js
new file mode 100644
index 000000000..78c85af63
--- /dev/null
+++ b/src/ducks/pages.js
@@ -0,0 +1,82 @@
+import fetchJSON from "../fetchJSON"
+
+// redux reducer
+export default function reducer(state = {}, action) {
+
+ switch (action.type) {
+ case "PAGE_GET":
+ return {
+ ...state,
+ [action.page]: {
+ loading: true,
+ },
+ }
+
+ case "PAGE_SET":
+ const data = action.response.data
+ return {
+ ...state,
+ [action.page]: {
+ ...data,
+ type: data.head ? data.head.layout || data.head.type : "Page",
+ },
+ }
+
+ case "PAGE_FORGET":
+ return {
+ ...state,
+ [action.page]: undefined,
+ }
+
+ case "PAGE_ERROR":
+ return {
+ ...state,
+ [action.page]: action.error.response
+ ? {
+ error: action.error.response.status,
+ errorText: action.error.response.statusText,
+ }
+ : {
+ ...action.error,
+ },
+ }
+
+ default:
+ return state
+ }
+}
+
+// redux actions
+export function get(page) {
+ return {
+ types: [
+ "PAGE_GET",
+ "PAGE_SET",
+ "PAGE_ERROR",
+ ],
+ page,
+ promise: fetchJSON(`/${ page }/index.json`),
+ }
+}
+
+export function setType(page, type) {
+ return {
+ type: "PAGE_TYPE",
+ page,
+ pageType: type,
+ }
+}
+
+export function unknownType(page, type) {
+ return {
+ type: "PAGE_ERROR",
+ page,
+ error: {
+ error: "Unkown page type",
+ errorText: (
+ `"${ type }" component not available in ` +
+ `"pageComponents" prop`
+ ),
+ },
+ }
+}
diff --git a/src/enhance-collection/index.js b/src/enhance-collection/index.js
new file mode 100644
index 000000000..e1dbae781
--- /dev/null
+++ b/src/enhance-collection/index.js
@@ -0,0 +1,111 @@
+export default function enhanceCollection(collection, options) {
+ options = {
+ addSiblingReferences: true,
+ ...options,
+ }
+
+ if (options.filter) {
+ collection = filter(collection, [ options.filter ])
+ }
+
+ if (options.filters) {
+ collection = filter(collection, options.filters)
+ }
+
+ if (options.sort) {
+ collection = sort(collection, options.sort)
+ }
+
+ if (options.reverse) {
+ collection = reverse(collection)
+ }
+
+ if (options.limit) {
+ collection = limit(collection, options.limit)
+ }
+
+ if (options.addSiblingReferences) {
+ collection = addSiblingReferences(collection)
+ }
+
+ return collection
+}
+
+export function filter(collection, filters) {
+ return collection.reduce((acc, item) => {
+ filters.forEach((filter) => {
+ switch (typeof filter) {
+ case "function":
+ if (filter(item)) {
+ acc.push(item)
+ }
+ break
+
+ case "object":
+ const keys = Object.keys(filter)
+ if (
+ keys.reduce(
+ (acc, key) => acc && item[key] === filter[key],
+ true
+ )
+ ) {
+ acc.push(item)
+ }
+ break
+
+ case "string":
+ default:
+ if (item[filter]) {
+ acc.push(item)
+ }
+ break
+
+ }
+ })
+
+ return acc
+ }, [])
+}
+
+export function sort(collection, sort = "date") {
+ collection = [ ...collection ]
+
+ if (typeof sort === "function") {
+ collection.sort(sort)
+ }
+ else {
+ collection.sort((a, b) => {
+ a = a[sort]
+ b = b[sort]
+ if (!a && !b) return 0
+ if (!a) return -1
+ if (!b) return 1
+ if (b > a) return -1
+ if (a > b) return 1
+ return 0
+ })
+ }
+
+ return collection
+}
+
+export function reverse(collection) {
+ collection = [ ...collection ]
+ collection.reverse()
+ return collection
+}
+
+export function limit(collection, limit) {
+ return collection.slice(0, limit)
+}
+
+export function addSiblingReferences(collection) {
+ const last = collection.length - 1
+ return collection.map((item, i) => {
+ return {
+ ...item,
+ ...(0 != i) && { previous: collection[i-1] },
+ ...(last != i) && { next: collection[i+1] },
+ }
+ })
+}
diff --git a/src/fetchJSON/index.js b/src/fetchJSON/index.js
new file mode 100644
index 000000000..e69501204
--- /dev/null
+++ b/src/fetchJSON/index.js
@@ -0,0 +1,33 @@
+export default function(url, data) {
+ return fetch(url, data)
+ .then((res) => {
+
+ // get response fields has a raw object
+ const response = {}
+ // using spread or Object.keys do not return computed properties
+ // hasOwnProperty doesn't help
+ // so good old `for in` that checks simple types
+ for (const key in res) {
+ if (typeof res[key] !== "function") {
+ response[key] = res[key]
+ }
+ }
+
+ // for both http ok/not ok, we try to parse as JSON
+ // or reject if json parsing failed
+
+ // http OK
+ if (res.status >= 200 && res.status < 300) {
+ return new Promise((resolve, reject) => res.json().then(
+ (data) => resolve({ response, data }),
+ (exception) => reject({ response, exception }),
+ ))
+ }
+
+ // http not ok
+ return new Promise((resolve, reject) => res.json().then(
+ (data) => reject({ response, data }),
+ (exception) => reject({ response, exception }),
+ ))
+ })
+}
diff --git a/src/json-collection-loader/cache.js b/src/json-collection-loader/cache.js
new file mode 100644
index 000000000..52cec4873
--- /dev/null
+++ b/src/json-collection-loader/cache.js
@@ -0,0 +1,9 @@
+let cache
+
+export function cleanCache() {
+ cache = []
+}
+
+cleanCache()
+
+export default cache
diff --git a/src/json-collection-loader/index.js b/src/json-collection-loader/index.js
new file mode 100644
index 000000000..16a493bfc
--- /dev/null
+++ b/src/json-collection-loader/index.js
@@ -0,0 +1,34 @@
+import loaderUtils from "loader-utils"
+
+import cache from "./cache"
+
+function relativePath(path) {
+ // remove cwd from resource path to relative path
+ const cwd = process.cwd()
+ if (path.indexOf(cwd) === 0) {
+ path = path.substr(cwd.length + 1)
+ }
+
+ return path
+}
+
+export default function(input) {
+ this.cacheable()
+
+ const query = loaderUtils.parseQuery(this.query)
+ const options = query.jsonCollection
+ ? query.jsonCollection
+ : this.options.jsonCollection
+ ? this.options.jsonCollection
+ : {
+ urlify: (url) => url,
+ }
+
+ cache.push({
+ __filename: relativePath(this.resourcePath),
+ __url: options.urlify(relativePath(this.resourcePath)),
+ ...JSON.parse(input).head,
+ })
+
+ return input
+}
diff --git a/src/json-collection-loader/package.json b/src/json-collection-loader/package.json
new file mode 100644
index 000000000..12b499d8b
--- /dev/null
+++ b/src/json-collection-loader/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "json-collection",
+ "version": "1.0.0",
+ "author": "Maxime Thirouin",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/MoOx/json-collection-loader"
+ },
+ "dependencies": {
+ "loader-utils": "^0.2.11"
+ }
+}
diff --git a/src/json-collection-loader/plugin.js b/src/json-collection-loader/plugin.js
new file mode 100644
index 000000000..e570d2bfc
--- /dev/null
+++ b/src/json-collection-loader/plugin.js
@@ -0,0 +1,28 @@
+import fs from "fs"
+import path from "path"
+import cache, { cleanCache } from "./cache"
+
+let lastOutput
+
+function jsonCollectionPlugin(options) {
+ return function() {
+ this.plugin("done", () => {
+ const jsonCollection = JSON.stringify(cache, null, 2)
+ if (lastOutput !== jsonCollection) {
+ lastOutput = jsonCollection
+ fs.writeFile(
+ path.join(this.outputPath, options.filename),
+ jsonCollection,
+ (err) => {
+ if (err) {
+ throw err
+ }
+ cleanCache()
+ }
+ )
+ }
+ })
+ }
+}
+
+export default jsonCollectionPlugin
diff --git a/src/markdown-as-json-loader/index.js b/src/markdown-as-json-loader/index.js
new file mode 100644
index 000000000..ada9ab457
--- /dev/null
+++ b/src/markdown-as-json-loader/index.js
@@ -0,0 +1,51 @@
+/*
+Example
+
+```md
+ ---
+ title: Test
+ key: value
+ ---
+
+ _Md_ content
+```
+
+return a object:
+
+```json
+{
+ head: {
+ title: "Test",
+ key: "value",
+ },
+ body: "Md content",
+ rawBody: "_Md_ content",
+ raw: {initial content},
+}
+
+```
+ */
+import loaderUtils from "loader-utils"
+
+import yamlHeaderParser from "gray-matter"
+import markdownIt from "markdown-it"
+
+export default function(source) {
+ this.cacheable()
+
+ const query = loaderUtils.parseQuery(this.query)
+ const md = query.markdownIt
+ ? query.markdownIt
+ : this.options.markdownIt
+ ? this.options.markdownIt
+ : markdownIt()
+
+ const parsed = yamlHeaderParser(source)
+
+ return JSON.stringify({
+ head: parsed.data,
+ body: md.render(parsed.content),
+ rawBody: parsed.content,
+ raw: parsed.orig,
+ })
+}
diff --git a/src/markdown-as-json-loader/package.json b/src/markdown-as-json-loader/package.json
new file mode 100644
index 000000000..81fe2d35a
--- /dev/null
+++ b/src/markdown-as-json-loader/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "markdown-as-json",
+ "version": "1.0.0",
+ "author": "Maxime Thirouin",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/MoOx/markdown-as-json-loader"
+ },
+ "dependencies": {
+ "loader-utils": "^0.2.11"
+ }
+}
diff --git a/src/to-static-html/Html.js b/src/to-static-html/Html.js
new file mode 100644
index 000000000..ec6595ffe
--- /dev/null
+++ b/src/to-static-html/Html.js
@@ -0,0 +1,47 @@
+import React, { Component } from "react"
+import { PropTypes } from "react"
+
+export default class Html extends Component {
+
+ static propTypes = {
+ body: PropTypes.string.isRequired,
+ store: PropTypes.object.isRequired,
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/src/to-static-html/index.js b/src/to-static-html/index.js
new file mode 100644
index 000000000..f83af0ebf
--- /dev/null
+++ b/src/to-static-html/index.js
@@ -0,0 +1,88 @@
+import fs from "fs"
+import path from "path"
+import mkdirp from "mkdirp"
+
+import urlAsHtml from "./url-as-html"
+
+import createStore from "../createStore"
+
+// import collection from "../json-collection-loader/cache"
+
+// react-router beta4
+// import { createRoutes } from "react-router/lib/RouteUtils"
+
+export default ({ urls, source, dest, exports }) => {
+
+ const store = createStore(exports.reducers, exports.initialState)
+
+ const routes =
+ // react-router beta4
+ // createRoutes(
+ exports.routes
+ // )
+
+ // create all html files
+ return Promise.all(
+ urls.map(
+ (url) => {
+ const basename = path.join(dest, url)
+ let data
+ try {
+ data = require(path.join(basename, "index"))
+ }
+ /* eslint-disable no-empty */
+ catch (err) {
+ // no
+ // console.info(`No data for url '${ url }'.`)
+ }
+ /* eslint-enable no-empty */
+
+ if (data) {
+ // prepare page data
+ store.dispatch({
+ type: "PAGE_SET",
+ page: url,
+ response: {
+ data,
+ },
+ })
+ }
+
+ return (
+ urlAsHtml(url, { routes, store })
+ .then(
+ (html) => {
+ return new Promise((resolve, reject) => {
+ const filename = path.join(basename, "index.html")
+ // console.log(basename, filename)
+
+ mkdirp(basename, (err) => {
+ // console.log("mkdir done", basename, err)
+ if (err) {
+ reject(err)
+ }
+
+ fs.writeFile(filename, html, (error) => {
+ // console.log("fs.writeFile done", filename, err)
+ if (error) {
+ reject(error)
+ }
+
+ // forget page data to avoid having all pages data in all
+ // pages
+ store.dispatch({
+ type: "PAGE_FORGET",
+ page: url,
+ })
+
+ resolve(filename)
+ })
+ })
+ })
+ }
+ )
+ )
+ }
+ )
+ )
+}
diff --git a/src/to-static-html/url-as-html.js b/src/to-static-html/url-as-html.js
new file mode 100644
index 000000000..a2b30b3c5
--- /dev/null
+++ b/src/to-static-html/url-as-html.js
@@ -0,0 +1,67 @@
+import React from "react"
+
+// react-router beta4
+// import { useRoutes, RoutingContext } from "react-router"
+// import createHistory from "history/lib/createMemoryHistory"
+// import createLocation from "history/lib/createLocation"
+import Router from "react-router"
+import Location from "react-router/lib/Location"
+
+import { Provider } from "react-redux"
+
+import Html from "./Html"
+
+export default (url, { routes, store }) => new Promise((resolve, reject) => {
+
+ try {
+ const location = new Location(url)
+ // react-router beta4
+ // https://github.com/rackt/react-router/issues/1793
+ // const location = createLocation(url)
+ // const history = useRoutes(createHistory)({ routes })
+ // history.match(
+ // react-router beta3
+ Router.run(
+ routes,
+ location,
+ (error, state) => {
+ if (error) {
+ return reject(error)
+ }
+
+ // write htmlString as html files
+ return resolve(
+ // render html document as simple html
+ "" +
+ React.renderToStaticMarkup(
+ React.createElement(Html, {
+
+ // but render app body as "react"ified html (with data-react-id)
+ body: React.renderToString(
+ // the wrapper is used here because the client might have the
+ // devtools at the same level as the
+ // the