├── .eslintrc ├── .gitignore ├── NOTES ├── HowStuffWorks.md ├── importsDiagram.png └── importsDiagram.txt ├── README.md ├── Vagrantfile ├── app ├── .eslintrc ├── actions.js ├── components │ ├── ChatLineView.jsx │ ├── ChatUserView.jsx │ ├── MainMenu.jsx │ ├── NewChatEditor.jsx │ ├── NewTodoItemEditor.jsx │ ├── README.md │ ├── TEMPLATE.jsx │ ├── TodoItem.jsx │ ├── TodoItemEditor.jsx │ └── TodoListMenu.jsx ├── containers │ ├── Application.css │ ├── Application.jsx │ ├── ChatList.css │ ├── ChatList.jsx │ ├── ChatPage.jsx │ ├── HomePage.jsx │ ├── NotFoundPage.jsx │ ├── README.md │ ├── ReadmePage.css │ ├── ReadmePage.jsx │ ├── SomePage.jsx │ ├── TEMPLATE.css │ ├── TEMPLATE.jsx │ ├── TodoItemPage.jsx │ ├── TodoListPage.jsx │ └── TodoPage.jsx ├── elements │ ├── README.md │ ├── ReactLogo.jsx │ ├── ReactLogo │ │ ├── logo.jpg │ │ ├── logo.png │ │ └── logo.svg │ └── TEMPLATE.jsx ├── fetch-helpers │ └── rest.js ├── mainPrerender.html ├── mainRoutes.jsx ├── mainStores.js ├── mainStoresDescriptions.js ├── route-handlers │ ├── Application.jsx │ ├── ChatPage.jsx │ ├── HomePage.jsx │ ├── NotFoundPage.jsx │ ├── README.md │ ├── ReadmePage.jsx │ ├── SomePage.jsx │ ├── TEMPLATE.jsx │ ├── TodoItemPage.jsx │ ├── TodoListPage.jsx │ ├── TodoPage.jsx │ └── async.js ├── simple.html ├── store-helpers │ ├── Chat.js │ └── Todo.js └── update-helpers │ ├── list.js │ └── react.js ├── build └── .empty ├── config ├── .eslintrc ├── Prerenderer.jsx ├── SimpleRenderer.js ├── StoresWrapper.jsx ├── createStoresForPrerender.jsx ├── loadersByExtension.js ├── mainApp.jsx ├── mainPrerenderer.jsx ├── renderApplication.jsx └── withTimeout.jsx ├── lib ├── DB.js ├── api.js ├── dbs.js ├── mainPrerenderApi.js ├── server-development.js ├── server-production.js └── server.js ├── make-webpack-config.js ├── package.json ├── public └── .empty ├── webpack-dev-server.config.js ├── webpack-hot-dev-server.config.js ├── webpack-production.config.js └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "strict": 0, 7 | "curly": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | .vagrant 4 | -------------------------------------------------------------------------------- /NOTES/HowStuffWorks.md: -------------------------------------------------------------------------------- 1 | ![importsDiagram](importsDiagram.png) 2 | 3 | *An arrow means: `imports`* 4 | 5 | # How Stuff Works 6 | This documentation is for grasping the overall design. No details by intention. To get up and running quickly see `README.md`. 7 | 8 | At first sight, it may not be immediately clear why/how certain things are called/triggered. 9 | The below explanation points you in the right direction, so you can research further. If you want more details or documentation, see the relevant package. 10 | Some (inconspicuously) used/relevant node modules are explicitly mentioned with package name. 11 | 12 | ## Table of contents 13 | 14 | * [How the app is served.](#how-the-app-is-served) 15 | * [development mode](#development-mode) 16 | * [production mode](#production-mode) 17 | * [How the page updates while you are programming.](#how-the-page-updates-while-you-are-programming) 18 | * [page reloading](#page-reloading) 19 | * [hot reloading](#hot-reloading) 20 | * [How the routes work.](#how-the-routes-work) 21 | * [How the server JSON API works.](#how-the-server-json-api-works) 22 | * [How the stores work.](#how-the-stores-work) 23 | * [Stores setup](#stores-setup) 24 | * [Stores in use](#stores-in-use) 25 | * [How the actions work.](#how-the-actions-work) 26 | * [How the server DB works.](#how-the-server-db-works) 27 | * [How the 'Random fail!' works.](#how-the-random-fail-works) 28 | * [How the build works.](#how-the-build-works) 29 | * [Q&A Why](#qa-why) 30 | 31 | ***** 32 | 33 | ## How the app is served. 34 | A JS webserver is included in the form of `lib/server.js`. It can be run in two modes: 35 | 36 | ### development mode 37 | Run the server using `npm run start-dev`. 38 | It will use `lib/server-development.js` which will use `lib/server.js` which will use `config/simple.js` which will use `app/simple.html`. A tiny HTML file is loaded, the JS is downloaded + executed + rendered, and ultimately output is shown. 39 | 40 | ### production mode 41 | In this mode, the React HTML output is (pre)rendered (and populated) on the server (a.k.a. isomorphic). 42 | 43 | Run the server using `npm run start`. 44 | It will use `lib/server-production.js` which will use `lib/server.js` which will use `config/mainPrerenderer.jsx` which will use `app/mainPrerender.html`. A big HTML file is loaded (and output is shown immediately), the JS is downloaded + executed + rendered, and output is updated. 45 | 46 | This server side render (prerender) is possible because the React JS can be executed on the server. 47 | In your browser the main `React.render(, document.getElementById("content"))` call (in `config/mainApp.jsx`) outputs to the browser DOM. 48 | But when pre-rendering, the main `React.renderToString()` call (in `app/prerender.jsx`) outputs to a string, which is inserted into the HTML (including React component states) as content. 49 | The browser now can show the HTML instantly in the DOM, but proceeds to run the React JS that resumes the usual DOM mutations. 50 | 51 | Note: Routes that are asynchronously loaded can not be pre-rendered. 52 | 53 | 54 | ## How the page updates while you are programming. 55 | After running the app webserver in development mode (see above) you'd have to manually reload the page after changing a JSX file. 56 | It is possible to automatically reload _or_ update the page to reflect your changes: 57 | 58 | ### page reloading 59 | Ensure that you are running the app webserver in development mode. 60 | 61 | Then run another server using `npm run dev-server`. 62 | It will rebuild while watching for file changes, and it will trigger your page to reload afterwards. 63 | 64 | ### hot reloading 65 | Ensure that you are running the app webserver in development mode. 66 | 67 | Then run another server using `npm run hot-dev-server`. 68 | It will rebuild while watching for file changes, and it will update the currently shown and affected component(s) while keeping their state. 69 | Note this is experimental, and in some cases you'll need to refresh manually. 70 | 71 | 72 | ## How the routes work. 73 | In React after opening the app and going to some page, there is no actual HTML loaded from the server. React app just replaces a component that acts as a page, including any child components. 74 | But you'd like to have the URL reflect this, and allow user to use browser history (back/forward). A router takes care of these things. (package react-router) 75 | 76 | In this case, the root of your app is not the Application React component. 77 | This starts at `lib/server.js` which will use `config/mainApp.jsx` which instantiates the router and ultimately uses `app/mainRoutes.jsx` to load routes. 78 | You'll find that all pages are subroutes within the `app` route, which instantiates `app/Application/index.jsx`, which contains a `` component that inserts subroute output. 79 | 80 | 81 | ## How the server JSON API works. 82 | The `lib/server.js` serves the application, but it also serves the API URLs (`lib/api.js`) that the stores talk with. 83 | It initializes two databases (todolist and todoitem) once, and then continues to listen for GET/POST requests on specific URLs. 84 | 85 | 86 | ## How the stores work. 87 | As in Flux; the Stores affect the React components. (package `items-store`) And the stores talk to the JSON API (`app/fetch-helpers/rest.js`). 88 | See [Q&A Why](#qa-why) 89 | 90 | ### Stores setup 91 | The stores are constructed as such: startpoint is `config/mainApp.jsx` which will use `app/mainStores.js`. This then: 92 | 93 | - defines routines to handle JSON API read/writes (package `superagent`), and 94 | - sets up a queue that only allows one REST request at a time, and aggregates subsequent changes. 95 | These are then sent as one request.. 96 | - and ultimately constructs the two respective `ItemStore` objects (based on `app/mainStoresDescriptions.js`) to which it assigns these routines, queue, and the initial data. This initial data may have been inserted via `app/prerender.html` and `app/`. 97 | - These are then exported back to `config/mainApp.jsx`, along with a store named `Router`. 98 | 99 | The store `Router` just holds a `transition` value which is later read in `app/containers/Application.jsx` to show "loading..." in UI. 100 | The stores `TodoList` and `TodoItem` only have minor differences. 101 | 102 | If you wonder where the default data is inserted, see (#how-the-server-db-works). 103 | 104 | ### Stores in use 105 | 106 | todo :question: this section needs review and should be extended. 107 | 108 | Note: Components should not access the stores except for reading in `getProps()`. Everything else should be done with actions. 109 | 110 | 111 | ## How the actions work. 112 | The Actions affect the stores. (package items-store) 113 | 114 | The actions are setup in `app/mainStores.js` (bottom) from `app/actions.js` which uses the implementation supplied with items-store. 115 | They are triggered/made by the input fields in containers, such as `app/containers/TodoListPage.jsx`. 116 | They end up affecting a store. See [How the stores work.](#how-the-stores-work) 117 | 118 | 119 | ## How the server DB works 120 | When you run `npm run start-dev` (or without `-dev` ofcourse) this will start the server, as you can see defined in `package.json`. This lets node execute `lib/server-development.js` which uses `lib/server.js` where the default data is loaded, and a server (package express) is thrown together that responds to GET POST and DELETE. 121 | 122 | This (REST API with JSON data format) server is accessible via `http://localhost:8080/_/list/mylist` for example, and this is what the application uses to fetch data for the stores. 123 | 124 | 125 | ## How the 'Random fail!' works. 126 | This [Chaos Monkey](https://github.com/Netflix/SimianArmy/wiki) lives in `lib/server.js` and helps you experience realistic server-client retrieval times and errors while developing. 127 | At some time your application is requesting 3 things from the server, and they return in the wrong order and incomplete. Will it break? 128 | Or a form could not be sent to the server. Will it notify the user? 129 | 130 | 131 | ## How the build works. 132 | A build can compile your JS files into one big file, while applying some optimisations (to make it smaller, faster, or obfuscated). 133 | Its also used to add files and features (otherwise not supported by JS in the browser) to your project, such as `JSX`, `markdown`, or `SASS`. 134 | 135 | When you start run a dev-server (like `npm run dev-server` or `npm run hot-dev-server`) it does the builds for you, while it watches for changing files. 136 | Or you can manually do a normal build using the `npm run build`. Note: these `npm run` scripts are defined in `package.json`. 137 | 138 | Depending on what way you used to do this build, a different build configuration is used. For example the normal build script as seen in `package.json` starts webpack with custom config `webpack-production.config.js` which uses shared config `make-webpack-config.js`. 139 | Most webpack configuration is in that shared config file, per entry. Only one main entry is defined. Pre-rendering happens depending on the custom config. 140 | This is where a lot of node modules (packages) come into play: loaders add JSX support, debug options are set, and the output is set to `build/` is set. 141 | 142 | # Q&A Why 143 | **Design choices explained.** 144 | Ok, so now you know the how. But why do it that way? Questions about design choices, answered by the author(s). (Tobias Koppers) 145 | 146 | **(interim, this document should only concern the How question) :exclamation: TODO extract Q&A info and document it.** 147 | 148 | What is the argument for using items-store? (from https://github.com/webpack/react-starter/pull/49 ) 149 | > Q: What was the argument for using items-store? 150 | > A: 151 | > 152 | > I didn't want to write a new module. I actually tried using Reflux.js, but couldn't find a good workflow for pre-rendering, optimistic updates and merging of multiple writes. You could do it manually but that is something I would expect from a flux implementation. items-store is a very simple store base class that coordinate this behavior (the repo actually also contains a simple helper for actions and a react mixin, but these are theoretically independent and you don't have to use them with the store). 153 | > 154 | > items-store allows to serialize and deserialize the store data to inject the data after pre-rendering. It manages optimistic updates and merges multiple writes. But items-store only offers a simple key-value store API and forces you to map more complex operations (via store-helpers) to this model. It's just a caching layer with events to the remote API (which need to be provided in the constructor). 155 | > 156 | > items-store basically provides a "raw data" store, while other implementations give you the ability to write higher level stores. In items-store you need to put this higher-level behavior in store-helpers. The advantage is that this way basic functionality is already provided by items-store. 157 | > 158 | > Now there is an alternative to items-store, which could provide "pre-rendering" part too: http://fluxible.io/ they use dehydrate and rehydrate to (de)serialize to data from the stores and provide it to the client stores. 159 | 160 | Regarding the paths that store data travels (from https://github.com/webpack/react-starter/pull/51 ) 161 | > Q: How is it triggered to refresh everything from the server, 162 | > A: `config/mainApp.jsx` invalidates store data when you change the page. When the pages read the data again it is invalid and will be refetched. 163 | 164 | > Q: how does it propagate changes when the user edits items, and 165 | > A: The component fires an action (from `app/actions.jsx`) on edit. The action is handled in `app/mainStores.jsx` and writes some items on stores. The stores update their internal cache (which is displayed on the page as optimistic update) and do server requests (through their registered handlers in `app/mainStores.jsx`. 166 | 167 | > Q: how do values travel before ending up in some components render() ? 168 | > A: component requests values from store in getProps() -> `app/mainStores.jsx` read (unavailable or invalidated) data from server -> internal `items-store` cache for the store -> getProps() -> components this.props -> render() 169 | 170 | > Q: how does the queue combine multiple requests? I cant imagine subsequent add/remove/edits would result in just one rest call.. do they? 171 | > A: items-store store a single update per entry. Any subsequent updateItem() call causes both updates to be merged (mergeUpdates from items-store). Requests from stores to the server are queued (queueRequest in app/mainStores.jsx). Here only a single ongoing request is allowed. All writes that happen in the meantime are merged into a single write. 172 | -------------------------------------------------------------------------------- /NOTES/importsDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/react-starter/16c0db3a3bad3cbf611007608339be31fc27bc8b/NOTES/importsDiagram.png -------------------------------------------------------------------------------- /NOTES/importsDiagram.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package client { 4 | cloud [config/mainApp] as app 5 | } 6 | 7 | package ui { 8 | component elements 9 | component components 10 | component containers 11 | component [route-handlers] as rh 12 | component mainRoutes 13 | } 14 | 15 | component [store-helpers] as sh 16 | component [fetch-helpers] as fh 17 | component [update-helpers] as uh 18 | 19 | component actions 20 | 21 | package stores { 22 | component mainStores 23 | component mainStoresDescriptions 24 | } 25 | 26 | package server { 27 | component [lib/server] as server 28 | component [lib/mainPrerenderApi] as prerenderApi 29 | component [config/mainPrerenderer] as mainPrerenderer 30 | component [lib/api] as api 31 | database "lib/db" as db 32 | } 33 | 34 | component [external styles/components] as ext 35 | 36 | 37 | 38 | server -right-> api 39 | server .down.> mainPrerenderer 40 | server -down-> prerenderApi 41 | api --right-> db 42 | prerenderApi -up-> db 43 | 44 | app ---> mainRoutes 45 | app ---> mainStores 46 | mainPrerenderer -----> mainRoutes 47 | mainPrerenderer -----> mainStoresDescriptions 48 | 49 | mainStores --> actions 50 | mainStores -right-> mainStoresDescriptions 51 | mainStores -down-> fh 52 | mainStoresDescriptions -down-> uh 53 | 54 | mainRoutes -right-> rh 55 | rh -right-> containers 56 | containers -right-> components 57 | containers -down-> containers 58 | components -> components 59 | components -right-> elements 60 | elements -> elements 61 | elements -up-> ext 62 | 63 | containers --> sh 64 | containers --> actions 65 | 66 | @enduml 67 | 68 | // http://www.planttext.com/planttext -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack/react-starter 2 | 3 | Starter template for react and webpack. 4 | 5 | ## Features 6 | 7 | * Compilation with webpack 8 | * React and jsx 9 | * react-router 10 | * Stylesheets can be CSS, LESS, SASS, Stylus or mixed 11 | * Embedded resources like images or fonts use DataUrls if appropriate 12 | * A simple flag loads a react component (and dependencies) on demand. 13 | * Development 14 | * Development server 15 | * Optionally Hot Module Replacement development server (LiveReload for Stylesheets and React components enabled) 16 | * Uses SourceUrl for performance, but you may switch to SourceMaps easily 17 | * Production 18 | * Server example for prerendering for React components 19 | * Initial data inlined in page 20 | * Long Term Caching through file hashes enabled 21 | * Generate separate css file to avoid FOUC 22 | * Minimized CSS and javascript 23 | * Also supports coffee-script files if you are more a coffee-script person. 24 | * You can also require markdown or text files for your content. 25 | 26 | ## Local Installation 27 | 28 | Install [node.js](https://nodejs.org) or [io.js](https://iojs.org) 29 | 30 | Just clone this repo and change the `origin` git remote. 31 | 32 | ``` text 33 | npm install 34 | ``` 35 | 36 | ## Installation via Vagrant 37 | 38 | Install [vagrant](https://vagrantup.com) 39 | 40 | ``` text 41 | vagrant up 42 | vagrant ssh 43 | cd /vagrant 44 | ``` 45 | 46 | ## Development server 47 | 48 | ``` text 49 | # start the webpack-dev-server 50 | npm run dev-server 51 | # wait for the first compilation is successful 52 | 53 | # in another terminal/console 54 | # start the node.js server in development mode 55 | npm run start-dev 56 | 57 | # open this url in your browser 58 | http://localhost:8080/ 59 | ``` 60 | 61 | The configuration is `webpack-dev-server.config.js`. 62 | 63 | It automatically recompiles and refreshes the page when files are changed. 64 | 65 | Also check the [webpack-dev-server documentation](http://webpack.github.io/docs/webpack-dev-server.html). 66 | 67 | 68 | ## Hot Module Replacement development server 69 | 70 | ``` text 71 | # start the webpack-dev-server in HMR mode 72 | npm run hot-dev-server 73 | # wait for the first compilation is successful 74 | 75 | # in another terminal/console 76 | # start the node.js server in development mode 77 | npm run start-dev 78 | 79 | # open this url in your browser 80 | http://localhost:8080/ 81 | ``` 82 | 83 | The configuration is `webpack-hot-dev-server.config.js`. 84 | 85 | It automatically recompiles when files are changed. When a hot-replacement-enabled file is changed (i. e. stylesheets or React components) the module is hot-replaced. If Hot Replacement is not possible the page is refreshed. 86 | 87 | Hot Module Replacement has a performance impact on compilation. 88 | 89 | 90 | ## Production compilation and server 91 | 92 | ``` text 93 | # build the client bundle and the prerendering bundle 94 | npm run build 95 | 96 | # start the node.js server in production mode 97 | npm run start 98 | 99 | # open this url in your browser 100 | http://localhost:8080/ 101 | ``` 102 | 103 | The configuration is `webpack-production.config.js`. 104 | 105 | The server is at `lib/server.js` 106 | 107 | The production setting builds two configurations: one for the client (`build/public`) and one for the serverside prerendering (`build/prerender`). 108 | 109 | 110 | ## Legacy static assets 111 | 112 | Assets in `public` are also served. 113 | 114 | 115 | ## Build visualization 116 | 117 | After a production build you may want to visualize your modules and chunks tree. 118 | 119 | Use the [analyse tool](http://webpack.github.io/analyse/) with the file at `build/stats.json`. 120 | 121 | 122 | ## Loaders and file types 123 | 124 | Many file types are preconfigured, but not every loader is installed. If you get an error like `Cannot find module "xxx-loader"`, you'll need to install the loader with `npm install xxx-loader --save` and restart the compilation. 125 | 126 | 127 | ## Common changes to the configuration 128 | 129 | ### Add more entry points 130 | 131 | (for a multi page app) 132 | 133 | 1. Add an entry point to `make-webpack-config.js` (`var entry`). 134 | 2. Add a new top-level react component in `app` (`xxxRoutes.js`, `xxxStoreDescriptions.js`, `xxxStores.js`). 135 | 3. (Optional) Enable `commonsChunk` in `webpack-production.config.js` and add `` to `app/prerender.html`. 136 | 4. Modify the server code to require, serve and prerender the other entry point. 137 | 5. Restart compilation. 138 | 139 | ### Switch devtool to SourceMaps 140 | 141 | Change `devtool` property in `webpack-dev-server.config.js` and `webpack-hot-dev-server.config.js` to `"source-map"` (better module names) or `"eval-source-map"` (faster compilation). 142 | 143 | SourceMaps have a performance impact on compilation. 144 | 145 | ### Enable SourceMaps in production 146 | 147 | 1. Uncomment the `devtool` line in `webpack-production.config.js`. 148 | 2. Make sure that the folder `build\public\debugging` is access controlled, i. e. by password. 149 | 150 | SourceMaps have a performance impact on compilation. 151 | 152 | SourceMaps contains your unminimized source code, so you need to restrict access to `build\public\debugging`. 153 | 154 | ### Coffeescript 155 | 156 | Coffeescript is not installed/enabled by default to not disturb non-coffee developer, but you can install it easily: 157 | 158 | 1. `npm install coffee-redux-loader --save` 159 | 2. In `make-webpack-config.js` add `".coffee"` to the `var extensions = ...` line. 160 | 161 | 162 | ## License 163 | 164 | Copyright (c) 2012-2015 Tobias Koppers [![Gittip donate button](http://img.shields.io/gittip/sokra.png)](https://www.gittip.com/sokra/) 165 | 166 | MIT (http://www.opensource.org/licenses/mit-license.php) 167 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | _script = <
CONTENT
2 | -------------------------------------------------------------------------------- /app/mainRoutes.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, DefaultRoute, NotFoundRoute } from "react-router"; 3 | 4 | /* eslint-disable no-multi-spaces */ 5 | // Only import from `route-handlers/*` 6 | import Application from "route-handlers/Application"; 7 | import SomePage from "route-handlers/SomePage"; 8 | import ReadmePage from "route-handlers/ReadmePage"; 9 | import TodoPage from "route-handlers/TodoPage"; 10 | import TodoListPage from "route-handlers/TodoListPage"; 11 | import TodoItemPage from "route-handlers/TodoItemPage"; 12 | import HomePage from "route-handlers/HomePage"; 13 | import NotFoundPage from "route-handlers/NotFoundPage"; 14 | import ChatPage from "route-handlers/ChatPage"; 15 | /* eslint-enable */ 16 | 17 | // polyfill 18 | if(!Object.assign) 19 | Object.assign = React.__spread; // eslint-disable-line no-underscore-dangle 20 | 21 | // export routes 22 | module.exports = ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | -------------------------------------------------------------------------------- /app/mainStores.js: -------------------------------------------------------------------------------- 1 | /*globals __StoreData */ 2 | 3 | // This file describe where stores read data from and where stores write data to. 4 | 5 | import ItemsStore from "items-store/ItemsStore"; 6 | import async from "async"; 7 | import { readSingleItem, writeAndReadSingleItem, readMultipleItems } from "fetch-helpers/rest"; 8 | 9 | // a queue that allows only one REST request at a time 10 | // it also defers the requests to next tick, to aggregate multiple changes 11 | var queue = async.queue(function(fn, callback) { 12 | process.nextTick(function() { 13 | fn(callback); 14 | }); 15 | }, 1); 16 | 17 | // load embedded initial store data from prerendering if available 18 | var initialData = typeof __StoreData === "object" ? __StoreData : {}; 19 | 20 | // take the store descriptions as base 21 | import desc from "./mainStoresDescriptions"; 22 | 23 | var stores; 24 | 25 | // helper methods to extract embedded data from results 26 | 27 | function todoListPlusItems(result) { 28 | Object.keys(result.items).forEach(function(key) { 29 | stores.TodoItem.setItemData(key.substr(1), result.items[key]); 30 | }); 31 | return result.list; 32 | } 33 | 34 | function chatRoomPlusUsers(result) { 35 | Object.keys(result.users).forEach(function(key) { 36 | stores.ChatUser.setItemData(key.substr(1), result.users[key]); 37 | }); 38 | return result.room; 39 | } 40 | 41 | // the stores 42 | stores = module.exports = { 43 | Router: new ItemsStore(desc.Router), 44 | 45 | TodoList: new ItemsStore({ 46 | // REST API at "/_/list/" (read/write) 47 | // the API also returns "TodoItem"s for requests 48 | 49 | writeAndReadSingleItem: writeAndReadSingleItem("/_/list/", todoListPlusItems), 50 | readSingleItem: readSingleItem("/_/list/", todoListPlusItems), 51 | 52 | queueRequest: queue.push.bind(queue), 53 | ...desc.TodoList 54 | }, initialData.TodoList), 55 | 56 | TodoItem: new ItemsStore({ 57 | // REST API at "/_/todo" (read/write) 58 | // it supports reading up to 10 items at once 59 | 60 | writeAndReadSingleItem: writeAndReadSingleItem("/_/todo/"), 61 | readSingleItem: readSingleItem("/_/todo/"), 62 | readMultipleItems: readMultipleItems("/_/todo/"), 63 | 64 | queueRequest: queue.push.bind(queue), 65 | maxWriteItems: 10, 66 | ...desc.TodoItem 67 | }, initialData.TodoItem), 68 | 69 | ChatRoom: new ItemsStore({ 70 | // REST API at "/_/chat-room" (read/write) 71 | // the API also returns "ChatUsers"s for requests 72 | 73 | writeAndReadSingleItem: writeAndReadSingleItem("/_/chat-room/", chatRoomPlusUsers), 74 | readSingleItem: readSingleItem("/_/chat-room/", chatRoomPlusUsers), 75 | 76 | queueRequest: queue.push.bind(queue), 77 | ...desc.ChatRoom 78 | }, initialData.ChatRoom), 79 | 80 | ChatUser: new ItemsStore({ 81 | // REST API at "/_/chat-user" (read only) 82 | 83 | readSingleItem: readSingleItem("/_/chat-user/"), 84 | 85 | queueRequest: queue.push.bind(queue), 86 | ...desc.ChatUser 87 | }, initialData.ChatUser) 88 | }; 89 | 90 | 91 | // bind actions to stores 92 | 93 | import { Todo, Chat } from "./actions"; 94 | 95 | Todo.fetch.listen(function() { 96 | stores.TodoList.update(); 97 | stores.TodoItem.update(); 98 | }); 99 | 100 | Todo.add.listen(function(list, item) { 101 | stores.TodoList.updateItem(list, { $push: [item] }); 102 | }); 103 | 104 | Todo.update.listen(function(id, update) { 105 | stores.TodoItem.updateItem(id, update); 106 | }); 107 | 108 | Todo.fetchItem.listen(function(id) { 109 | stores.TodoItem.update(id); 110 | }); 111 | 112 | Chat.fetch.listen(function() { 113 | stores.ChatRoom.update(); 114 | stores.ChatUser.update(); 115 | }); 116 | 117 | Chat.send.listen((room, msg) => { 118 | stores.ChatRoom.updateItem(room, [msg]); 119 | }); 120 | -------------------------------------------------------------------------------- /app/mainStoresDescriptions.js: -------------------------------------------------------------------------------- 1 | 2 | // This file describe which stores exists and in which format updates are stored and merged 3 | 4 | import { applyUpdate as reactApplyUpdate, mergeUpdates as reactMergeUpdates } from "update-helpers/react"; 5 | import { applyUpdate as listApplyUpdate, mergeUpdates as listMergeUpdates } from "update-helpers/list"; 6 | 7 | module.exports = { 8 | // the Router is a local store that handles information about data fetching 9 | // see ../config/app.jsx 10 | Router: { 11 | local: true, 12 | readSingleItem: function(item, callback) { 13 | callback(null, item.oldData || null); 14 | } 15 | }, 16 | 17 | // stores TodoLists 18 | // changes are react style updates 19 | TodoList: { 20 | applyUpdate: reactApplyUpdate, 21 | mergeUpdates: reactMergeUpdates 22 | }, 23 | 24 | // stores TodoItems 25 | // changes are in the default format 26 | TodoItem: {}, 27 | 28 | // stores chats in a chat room 29 | // changes are lists of new messages 30 | // errors result in a error item 31 | ChatRoom: { 32 | applyUpdate: listApplyUpdate, 33 | mergeUpdates: listMergeUpdates, 34 | applyNewError: (oldData, error) => { 35 | var errorMessage = { 36 | user: "System", 37 | message: error.message 38 | }; 39 | return (oldData || []).concat(errorMessage); 40 | } 41 | }, 42 | 43 | // stores information about each chat user 44 | // currently this only stores the message count 45 | // uses defaults for everything (simple key-value data) 46 | ChatUser: {} 47 | }; 48 | -------------------------------------------------------------------------------- /app/route-handlers/Application.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import Application from "containers/Application"; 3 | 4 | export default createContainer(Application); 5 | -------------------------------------------------------------------------------- /app/route-handlers/ChatPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import ChatPage from "containers/ChatPage"; 3 | 4 | export default createContainer(ChatPage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import HomePage from "containers/HomePage"; 3 | 4 | export default createContainer(HomePage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import NotFoundPage from "containers/NotFoundPage"; 3 | 4 | export default createContainer(NotFoundPage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/README.md: -------------------------------------------------------------------------------- 1 | # route-handlers 2 | 3 | > These handlers are bound to Routes in `../mainRoutes`. 4 | 5 | A route handler should do nothing, but only bind the handler to a technology. 6 | 7 | A component is allowed to import the following stuff: 8 | * `containers/*` 9 | 10 | Don't import: 11 | * `element/*` 12 | * `components/*` 13 | * `store-helpers/*` 14 | * `route-handlers/*` 15 | * `actions/*` 16 | 17 | Currently all route handlers are implemented by `items-store` containers. 18 | 19 | The technology can be easily replaced on per route handler level. This allow easy migration to a future technology. 20 | -------------------------------------------------------------------------------- /app/route-handlers/ReadmePage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import ReadmePage from "containers/ReadmePage"; 3 | 4 | export default createContainer(ReadmePage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/SomePage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import SomePage from "containers/SomePage"; 3 | 4 | export default createContainer(SomePage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/TEMPLATE.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import TEMPLATE from "containers/TEMPLATE"; 3 | 4 | export default createContainer(TEMPLATE); 5 | -------------------------------------------------------------------------------- /app/route-handlers/TodoItemPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import TodoItemPage from "containers/TodoItemPage"; 3 | 4 | export default createContainer(TodoItemPage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/TodoListPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import TodoListPage from "containers/TodoListPage"; 3 | 4 | export default createContainer(TodoListPage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/TodoPage.jsx: -------------------------------------------------------------------------------- 1 | import { createContainer } from "items-store"; 2 | import TodoPage from "containers/TodoPage"; 3 | 4 | export default createContainer(TodoPage); 5 | -------------------------------------------------------------------------------- /app/route-handlers/async.js: -------------------------------------------------------------------------------- 1 | // Specify which route handlers should be loaded async. 2 | // the configuration requires this file and adds the 3 | // react-proxy-loader for specified files 4 | module.exports = ["SomePage", "ReadmePage"]; 5 | -------------------------------------------------------------------------------- /app/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /app/store-helpers/Chat.js: -------------------------------------------------------------------------------- 1 | export function fetchChatUser(stores, name) { 2 | return { 3 | ...stores.ChatUser.getItem(name), 4 | name: name 5 | }; 6 | } 7 | 8 | export function fetchChatMessage(stores, data) { 9 | return { 10 | ...data, 11 | user: data.user && fetchChatUser(stores, data.user) 12 | }; 13 | } 14 | 15 | export function fetchChatMessages(stores, room) { 16 | var messages = stores.ChatRoom.getItem(room); 17 | if(!messages) return messages; 18 | return messages.map((msg) => fetchChatMessage(stores, msg)); 19 | } 20 | -------------------------------------------------------------------------------- /app/store-helpers/Todo.js: -------------------------------------------------------------------------------- 1 | export function fetchTodoItem(stores, item) { 2 | return { 3 | ...stores.TodoItem.getItem(item), 4 | ...stores.TodoItem.getItemInfo(item), 5 | id: item 6 | }; 7 | } 8 | 9 | export function fetchTodoList(stores, list) { 10 | var data = { 11 | items: stores.TodoList.getItem(list), 12 | ...stores.TodoList.getItemInfo(list), 13 | id: list 14 | }; 15 | var newId = 0; 16 | if(data.items) { 17 | data.items = data.items.map((item) => { 18 | if(typeof item === "object") return { 19 | ...item, 20 | sending: true, 21 | id: "new" + (newId++) 22 | }; 23 | return fetchTodoItem(stores, item); 24 | }); 25 | } 26 | return data; 27 | } 28 | -------------------------------------------------------------------------------- /app/update-helpers/list.js: -------------------------------------------------------------------------------- 1 | export function mergeUpdates(a, b) { 2 | return a.concat(b); 3 | } 4 | 5 | export function applyUpdate(oldData, update) { 6 | return oldData.concat(update.map((u) => ({ sending: true, ...u }))); 7 | } 8 | -------------------------------------------------------------------------------- /app/update-helpers/react.js: -------------------------------------------------------------------------------- 1 | // a helper method for merging react style updates 2 | // (not totally correct, but fine for now) 3 | export function mergeUpdates(a, b) { 4 | if(typeof a === "object" && typeof b === "object") { 5 | var res = {}; 6 | Object.keys(a).concat(Object.keys(b)).forEach(function(key) { 7 | if(a[key] && b[key]) { 8 | switch(key) { 9 | case "$push": 10 | res[key] = a[key].concat(b[key]); 11 | break; 12 | case "$unshift": 13 | res[key] = b[key].concat(a[key]); 14 | break; 15 | case "$splice": 16 | res[key] = a[key].concat(b[key]); 17 | break; 18 | case "$set": 19 | res[key] = b[key]; 20 | break; 21 | case "$merge": 22 | var o = res[key] = {}; 23 | Object.keys(a[key]).forEach(function(x) { 24 | o[x] = a[key][x]; 25 | }); 26 | Object.keys(b[key]).forEach(function(x) { 27 | o[x] = b[key][x]; 28 | }); 29 | break; 30 | } 31 | res[key] = mergeUpdates(a[key], b[key]); 32 | } else if(a[key]) 33 | res[key] = a[key]; 34 | else 35 | res[key] = b[key]; 36 | }); 37 | } 38 | return a || b; 39 | } 40 | 41 | export { default as applyUpdate } from "react/lib/update"; 42 | -------------------------------------------------------------------------------- /build/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/react-starter/16c0db3a3bad3cbf611007608339be31fc27bc8b/build/.empty -------------------------------------------------------------------------------- /config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "ecmaFeatures": { 8 | "arrowFunctions": true, 9 | "blockBindings": true, 10 | "classes": true, 11 | "defaultParams": true, 12 | "destructuring": true, 13 | "forOf": true, 14 | "modules": true, 15 | "objectLiteralComputedProperties": true, 16 | "objectLiteralShorthandMethods": true, 17 | "objectLiteralShorthandProperties": true, 18 | "spread": true, 19 | "superInFunctions": true, 20 | "templateStrings": true, 21 | "unicodeCodePointEscapes": true, 22 | "jsx": true 23 | }, 24 | "rules": { 25 | "react/jsx-boolean-value": 2, 26 | "react/jsx-quotes": 2, 27 | "react/jsx-no-undef": 2, 28 | "react/jsx-sort-props": 0, 29 | "react/jsx-sort-prop-types": 0, 30 | "react/jsx-uses-react": 2, 31 | "react/jsx-uses-vars": 2, 32 | "react/no-did-mount-set-state": 2, 33 | "react/no-did-update-set-state": 2, 34 | "react/no-multi-comp": 2, 35 | "react/no-unknown-property": 1, 36 | "react/prop-types": 1, 37 | "react/react-in-jsx-scope": 2, 38 | "react/self-closing-comp": 2, 39 | "react/wrap-multilines": 0 40 | }, 41 | "plugins": [ 42 | "react" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /config/Prerenderer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Router from "react-router"; 3 | import async from "async"; 4 | import StoresWrapper from "./StoresWrapper"; 5 | 6 | export default class Prerenderer { 7 | constructor(routes) { 8 | this.routes = routes; 9 | } 10 | 11 | getContent(path, stores, callback) { 12 | // run the path thought react-router 13 | Router.run(this.routes, path, function(Application, state) { 14 | // wait until every store is charged by the components 15 | // for faster response time there could be a timeout here 16 | async.forEach(state.routes, (route, innerCallback) => { 17 | if(route.handler.chargeStores) { 18 | route.handler.chargeStores(stores, state.params, innerCallback); 19 | } else { 20 | innerCallback(); 21 | } 22 | }, () => { 23 | 24 | // prerender the application with the stores 25 | var application = React.renderToString(); 26 | 27 | // get the data from the stores for embedding into the page 28 | var data = Object.keys(stores).reduce(function(obj, name) { 29 | if(!stores[name].desc.local) 30 | obj[name] = stores[name].getData(); 31 | return obj; 32 | }, {}); 33 | 34 | // format the full page 35 | callback(null, application, data); 36 | }); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/SimpleRenderer.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var path = require("path"); 3 | var html = fs.readFileSync(path.resolve(__dirname, "../app/simple.html"), "utf-8"); 4 | 5 | function SimpleRenderer(options) { 6 | this.html = html.replace("SCRIPT_URL", options.scriptUrl); 7 | } 8 | 9 | SimpleRenderer.prototype.render = function(_path, _readItems, callback) { 10 | callback(null, this.html); 11 | }; 12 | 13 | module.exports = SimpleRenderer; 14 | -------------------------------------------------------------------------------- /config/StoresWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class StoresWrapper { 4 | getChildContext() { 5 | return { 6 | stores: this.props.stores 7 | }; 8 | } 9 | 10 | render() { 11 | var Application = this.props.Component; 12 | return ; 13 | } 14 | }; 15 | 16 | StoresWrapper.childContextTypes = { 17 | stores: React.PropTypes.object 18 | }; -------------------------------------------------------------------------------- /config/createStoresForPrerender.jsx: -------------------------------------------------------------------------------- 1 | import { ItemsStore } from "items-store"; 2 | 3 | // create stores for prerending 4 | // readItems contains async methods for fetching the data from database 5 | export default function createStoresForPrerender(storesDescriptions, readItems) { 6 | return Object.keys(storesDescriptions).reduce(function(obj, name) { 7 | obj[name] = new ItemsStore(Object.assign({ 8 | readSingleItem: readItems[name], 9 | queueRequest: function(fn) { fn(); } 10 | }, storesDescriptions[name])); 11 | return obj; 12 | }, {}); 13 | } 14 | -------------------------------------------------------------------------------- /config/loadersByExtension.js: -------------------------------------------------------------------------------- 1 | function extsToRegExp(exts) { 2 | return new RegExp("\\.(" + exts.map(function(ext) { 3 | return ext.replace(/\./g, "\\."); 4 | }).join("|") + ")(\\?.*)?$"); 5 | } 6 | 7 | module.exports = function loadersByExtension(obj) { 8 | var loaders = []; 9 | Object.keys(obj).forEach(function(key) { 10 | var exts = key.split("|"); 11 | var value = obj[key]; 12 | var entry = { 13 | extensions: exts, 14 | test: extsToRegExp(exts) 15 | }; 16 | if(Array.isArray(value)) { 17 | entry.loaders = value; 18 | } else if(typeof value === "string") { 19 | entry.loader = value; 20 | } else { 21 | Object.keys(value).forEach(function(valueKey) { 22 | entry[valueKey] = value[valueKey]; 23 | }); 24 | } 25 | loaders.push(entry); 26 | }); 27 | return loaders; 28 | }; 29 | -------------------------------------------------------------------------------- /config/mainApp.jsx: -------------------------------------------------------------------------------- 1 | import routes from "../app/mainRoutes"; 2 | import stores from "../app/mainStores"; 3 | import renderApplication from "./renderApplication"; 4 | 5 | renderApplication(routes, stores, { 6 | timeout: 600 7 | }); 8 | -------------------------------------------------------------------------------- /config/mainPrerenderer.jsx: -------------------------------------------------------------------------------- 1 | import routes from "../app/mainRoutes"; 2 | import storesDescriptions from "../app/mainStoresDescriptions"; 3 | import html from "../app/mainPrerender.html"; 4 | import createStoresForPrerender from "./createStoresForPrerender"; 5 | import Prerenderer from "./Prerenderer"; 6 | 7 | export default class MainRenderer { 8 | constructor(options) { 9 | this.prerenderer = new Prerenderer(routes); 10 | this.html = html 11 | .replace("STYLE_URL", options.styleUrl) 12 | .replace("SCRIPT_URL", options.scriptUrl) 13 | .replace("COMMONS_URL", options.commonsUrl); 14 | } 15 | 16 | render(path, readItems, callback) { 17 | var stores = createStoresForPrerender(storesDescriptions, readItems); 18 | this.prerenderer.getContent(path, stores, (err, content, data) => { 19 | if(err) return callback(err); 20 | var page = this.html 21 | .replace("DATA", JSON.stringify(data)) 22 | .replace("CONTENT", content); 23 | callback(null, page); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /config/renderApplication.jsx: -------------------------------------------------------------------------------- 1 | import async from "async"; 2 | import React from "react"; 3 | import Router from "react-router"; 4 | import withTimeout from "./withTimeout"; 5 | import ReactUpdates from "react/lib/ReactUpdates"; 6 | import StoresWrapper from "./StoresWrapper"; 7 | 8 | export default function renderApplication(routes, stores, options) { 9 | var timeout = options.timeout || 600; 10 | 11 | var initialRun = true; 12 | 13 | // react-router handles location 14 | Router.run(routes, Router.HistoryLocation, function(Application, state) { 15 | 16 | // On every page navigation invalidate data from the stores 17 | // This is not needed when the server notifies the client about changes (WebSocket, SSE) 18 | if(!initialRun) { 19 | Object.keys(stores).forEach(function(key) { 20 | stores[key].outdate(); 21 | }); 22 | } 23 | initialRun = false; 24 | 25 | ReactUpdates.batchedUpdates(function() { 26 | stores.Router.setItemData("transition", state); 27 | }); 28 | 29 | // try to fetch data for a defined timespan 30 | // when the data is not fully fetched after the timeout components are rendered (with missing/old data) 31 | withTimeout(async.forEach.bind(async, state.routes, function(route, callback) { 32 | if(route.handler.chargeStores) { 33 | route.handler.chargeStores(stores, state.params, callback); 34 | } else { 35 | callback(); 36 | } 37 | }), timeout, function() { 38 | 39 | ReactUpdates.batchedUpdates(function() { 40 | stores.Router.setItemData("transition", null); 41 | }); 42 | 43 | // Render the components with the stores 44 | React.render( 45 | , 46 | document.getElementById("content") 47 | ); 48 | }); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /config/withTimeout.jsx: -------------------------------------------------------------------------------- 1 | export default function(fn, timeout, callback) { 2 | var timedOut = false; 3 | var to = setTimeout(function() { 4 | timedOut = true; 5 | callback(); 6 | }, timeout); 7 | fn(function() { 8 | clearTimeout(to); 9 | if(!timedOut) callback(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /lib/DB.js: -------------------------------------------------------------------------------- 1 | var update = require("react/lib/update"); 2 | 3 | function DB(initialData) { 4 | this.data = initialData || {}; 5 | } 6 | 7 | module.exports = DB; 8 | 9 | DB.prototype.get = function(id, createDefaultData) { 10 | var d = this.data["_" + id]; 11 | if(!d) { 12 | this.data["_" + id] = createDefaultData; 13 | return createDefaultData; 14 | } 15 | return d; 16 | }; 17 | 18 | DB.prototype.update = function(id, upd) { 19 | var res = this.data["_" + id] = update(this.data["_" + id], upd); 20 | return res; 21 | }; 22 | 23 | DB.prototype.set = function(id, data) { 24 | var res = this.data["_" + id] = data; 25 | return res; 26 | }; 27 | 28 | DB.prototype.getIds = function() { 29 | return Object.keys(this.data).map(function(key) { 30 | return key.substr(1); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | var uuid = require("uuid"); 2 | 3 | var dbs = require("./dbs"); 4 | var todosDb = dbs.todos; 5 | var listsDb = dbs.lists; 6 | var chatRoomsDb = dbs.chatRooms; 7 | var chatUsersDb = dbs.chatUsers; 8 | 9 | module.exports = function(app) { 10 | 11 | // REST APIs 12 | // Note that there is no security in this example 13 | // Make sure your production server handles requests better! 14 | 15 | app.get("/_/list/*", function(req, res) { 16 | var listParam = req.params[0]; 17 | res.setHeader("Content-Type", "application/json"); 18 | var list = listsDb.get(listParam, []); 19 | var items = {}; 20 | list.forEach(function(itemId) { 21 | items["_" + itemId] = todosDb.get(itemId, {}); 22 | }); 23 | res.end(JSON.stringify({ 24 | list: list, 25 | items: items 26 | })); 27 | }); 28 | 29 | app.post("/_/list/*", function(req, res) { 30 | var list = req.params[0]; 31 | res.setHeader("Content-Type", "application/json"); 32 | var newList = listsDb.update(list, req.body); 33 | var additionalItems = {}; 34 | newList = newList.map(function(item) { 35 | if(typeof item === "string") return item; 36 | var newId = uuid.v4(); 37 | todosDb.set(newId, item); 38 | additionalItems["_" + newId] = item; 39 | return newId; 40 | }); 41 | listsDb.set(list, newList); 42 | res.end(JSON.stringify({ 43 | list: newList, 44 | items: additionalItems 45 | })); 46 | }); 47 | 48 | app.get("/_/todo/*", function(req, res) { 49 | var todos = req.params[0].split("+"); 50 | res.setHeader("Content-Type", "application/json"); 51 | var data; 52 | if(todos.length === 1) { 53 | data = todosDb.get(todos[0], {}); 54 | } else { 55 | data = todos.reduce(function(obj, todo) { 56 | obj["_" + todo] = todosDb.get(todo, {}); 57 | return obj; 58 | }, {}); 59 | } 60 | res.end(JSON.stringify(data)); 61 | }); 62 | 63 | app.post("/_/todo/*", function(req, res) { 64 | var todo = req.params[0]; 65 | res.setHeader("Content-Type", "application/json"); 66 | res.end(JSON.stringify(todosDb.update(todo, {$merge: req.body}))); 67 | }); 68 | 69 | app.get("/_/chat-room/*", function(req, res) { 70 | var roomParam = req.params[0]; 71 | res.setHeader("Content-Type", "application/json"); 72 | var room = chatRoomsDb.get(roomParam, []); 73 | var users = {}; 74 | room.forEach(function(message) { 75 | var user = message.user; 76 | if(!users["_" + user]) 77 | users["_" + user] = chatUsersDb.get(user); 78 | }); 79 | res.end(JSON.stringify({ 80 | users: users, 81 | room: room 82 | })); 83 | }); 84 | 85 | app.post("/_/chat-room/*", function(req, res) { 86 | var roomParam = req.params[0]; 87 | res.setHeader("Content-Type", "application/json"); 88 | var newMessages = req.body; 89 | var room = chatRoomsDb.update(roomParam, {$push: newMessages}); 90 | var users = {}; 91 | room.forEach(function(message) { 92 | var user = message.user; 93 | if(!users["_" + user]) 94 | users["_" + user] = chatUsersDb.get(user); 95 | }); 96 | res.end(JSON.stringify({ 97 | room: room, 98 | users: users 99 | })); 100 | }); 101 | 102 | app.get("/_/chat-user/*", function(req, res) { 103 | var user = req.params[0]; 104 | res.setHeader("Content-Type", "application/json"); 105 | var data = chatUsersDb.get(user); 106 | res.end(JSON.stringify(data)); 107 | }); 108 | 109 | }; 110 | -------------------------------------------------------------------------------- /lib/dbs.js: -------------------------------------------------------------------------------- 1 | var uuid = require("uuid"); 2 | var DB = require("./DB"); 3 | 4 | // The fake database 5 | var todosDb = new DB(); 6 | var listsDb = new DB(); 7 | var chatRoomsDb = new DB(); 8 | 9 | function initData() { 10 | // Initial data 11 | var mylist = [uuid.v4(), uuid.v4(), uuid.v4()]; 12 | var otherlist = [uuid.v4()]; 13 | listsDb.set("mylist", mylist); 14 | listsDb.set("otherlist", otherlist); 15 | todosDb.set(mylist[0], { 16 | text: "Hello World" 17 | }); 18 | todosDb.set(mylist[1], { 19 | text: "Eat something" 20 | }); 21 | todosDb.set(mylist[2], { 22 | text: "Nothing" 23 | }); 24 | todosDb.set(otherlist[0], { 25 | text: "12345679" 26 | }); 27 | 28 | chatRoomsDb.set("home", [ 29 | { 30 | user: "bot", 31 | message: "Welcome" 32 | } 33 | ]); 34 | chatRoomsDb.set("room1", []); 35 | } 36 | 37 | initData(); 38 | 39 | exports.todos = todosDb; 40 | exports.lists = listsDb; 41 | exports.chatRooms = chatRoomsDb; 42 | exports.chatUsers = { 43 | get: function(name) { 44 | var count = chatRoomsDb.getIds().map(function(room) { 45 | return chatRoomsDb.get(room).filter(function(message) { 46 | return message.user === name; 47 | }).length; 48 | }).reduce(function(a, b) { 49 | return a + b; 50 | }, 0); 51 | return { 52 | name: name, 53 | messages: count 54 | }; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /lib/mainPrerenderApi.js: -------------------------------------------------------------------------------- 1 | var dbs = require("./dbs"); 2 | var todosDb = dbs.todos; 3 | var listsDb = dbs.lists; 4 | var chatRoomsDb = dbs.chatRooms; 5 | var chatUsersDb = dbs.chatUsers; 6 | 7 | module.exports = function(/* req */) { 8 | var api = {}; 9 | 10 | api.TodoItem = function(item, callback) { 11 | callback(null, todosDb.get(item.id, {})); 12 | }; 13 | 14 | api.TodoList = function(item, callback) { 15 | callback(null, listsDb.get(item.id, [])); 16 | }; 17 | 18 | api.ChatRoom = function(item, callback) { 19 | callback(null, chatRoomsDb.get(item.id, [])); 20 | }; 21 | 22 | api.ChatUser = function(item, callback) { 23 | callback(null, chatUsersDb.get(item.id)); 24 | }; 25 | 26 | return api; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/server-development.js: -------------------------------------------------------------------------------- 1 | require("./server")({ 2 | defaultPort: 8080 3 | }); 4 | -------------------------------------------------------------------------------- /lib/server-production.js: -------------------------------------------------------------------------------- 1 | require("./server")({ 2 | prerender: true, 3 | separateStylesheet: true, 4 | defaultPort: 8080 5 | }); 6 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | module.exports = function(options) { 2 | 3 | var express = require("express"); 4 | var bodyParser = require("body-parser"); 5 | var path = require("path"); 6 | 7 | // require the page rendering logic 8 | var Renderer = options.prerender ? 9 | require("../build/prerender/main.js") : 10 | require("../config/SimpleRenderer.js"); 11 | 12 | // load bundle information from stats 13 | var stats = require("../build/stats.json"); 14 | 15 | var createPrerenderApi = require("./mainPrerenderApi"); 16 | 17 | var publicPath = stats.publicPath; 18 | 19 | var renderer = new Renderer({ 20 | styleUrl: options.separateStylesheet && (publicPath + "main.css?" + stats.hash), 21 | scriptUrl: publicPath + [].concat(stats.assetsByChunkName.main)[0], 22 | commonsUrl: publicPath + [].concat(stats.assetsByChunkName.commons)[0] 23 | }); 24 | 25 | var app = express(); 26 | 27 | // serve the static assets 28 | app.use("/_assets", express.static(path.join(__dirname, "..", "build", "public"), { 29 | maxAge: "200d" // We can cache them as they include hashes 30 | })); 31 | app.use("/", express.static(path.join(__dirname, "..", "public"), { 32 | })); 33 | 34 | // artifical delay and errors 35 | app.use(function(req, res, next) { 36 | if(Math.random() < 0.05) { 37 | // Randomly fail to test error handling 38 | res.statusCode = 500; 39 | res.end("Random fail! (you may remove this code in your app)"); 40 | return; 41 | } 42 | setTimeout(next, Math.ceil(Math.random() * 1000)); 43 | }); 44 | 45 | app.use(bodyParser.json()); 46 | 47 | // load REST API 48 | require("./api")(app); 49 | 50 | // application 51 | app.get("/*", function(req, res) { 52 | renderer.render( 53 | req.path, 54 | createPrerenderApi(req), 55 | function(err, html) { 56 | if(err) { 57 | res.statusCode = 500; 58 | res.contentType = "text; charset=utf8"; 59 | res.end(err.message); 60 | return; 61 | } 62 | res.contentType = "text/html; charset=utf8"; 63 | res.end(html); 64 | } 65 | ); 66 | }); 67 | 68 | 69 | var port = process.env.PORT || options.defaultPort || 8080; 70 | app.listen(port, function() { 71 | console.log("Server listening on port " + port); 72 | }); 73 | }; 74 | -------------------------------------------------------------------------------- /make-webpack-config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | var StatsPlugin = require("stats-webpack-plugin"); 5 | var loadersByExtension = require("./config/loadersByExtension"); 6 | 7 | module.exports = function(options) { 8 | var entry = { 9 | main: options.prerender ? "./config/mainPrerenderer" : "./config/mainApp" 10 | // second: options.prerender ? "./config/secondPrerenderer" : "./config/secondApp" 11 | }; 12 | var loaders = { 13 | "jsx": options.hotComponents ? ["react-hot-loader", "babel-loader?stage=0"] : "babel-loader?stage=0", 14 | "js": { 15 | loader: "babel-loader?stage=0", 16 | include: path.join(__dirname, "app") 17 | }, 18 | "json": "json-loader", 19 | "coffee": "coffee-redux-loader", 20 | "json5": "json5-loader", 21 | "txt": "raw-loader", 22 | "png|jpg|jpeg|gif|svg": "url-loader?limit=10000", 23 | "woff|woff2": "url-loader?limit=100000", 24 | "ttf|eot": "file-loader", 25 | "wav|mp3": "file-loader", 26 | "html": "html-loader", 27 | "md|markdown": ["html-loader", "markdown-loader"] 28 | }; 29 | var cssLoader = options.minimize ? "css-loader?module" : "css-loader?module&localIdentName=[path][name]---[local]---[hash:base64:5]"; 30 | var stylesheetLoaders = { 31 | "css": cssLoader, 32 | "less": [cssLoader, "less-loader"], 33 | "styl": [cssLoader, "stylus-loader"], 34 | "scss|sass": [cssLoader, "sass-loader"] 35 | }; 36 | var additionalLoaders = [ 37 | // { test: /some-reg-exp$/, loader: "any-loader" } 38 | ]; 39 | var alias = { 40 | 41 | }; 42 | var aliasLoader = { 43 | 44 | }; 45 | var externals = [ 46 | 47 | ]; 48 | var modulesDirectories = ["web_modules", "node_modules"]; 49 | var extensions = ["", ".web.js", ".js", ".jsx"]; 50 | var root = path.join(__dirname, "app"); 51 | var publicPath = options.devServer ? 52 | "http://localhost:2992/_assets/" : 53 | "/_assets/"; 54 | var output = { 55 | path: path.join(__dirname, "build", options.prerender ? "prerender" : "public"), 56 | publicPath: publicPath, 57 | filename: "[name].js" + (options.longTermCaching && !options.prerender ? "?[chunkhash]" : ""), 58 | chunkFilename: (options.devServer ? "[id].js" : "[name].js") + (options.longTermCaching && !options.prerender ? "?[chunkhash]" : ""), 59 | sourceMapFilename: "debugging/[file].map", 60 | libraryTarget: options.prerender ? "commonjs2" : undefined, 61 | pathinfo: options.debug || options.prerender 62 | }; 63 | var excludeFromStats = [ 64 | /node_modules[\\\/]react(-router)?[\\\/]/, 65 | /node_modules[\\\/]items-store[\\\/]/ 66 | ]; 67 | var plugins = [ 68 | new webpack.PrefetchPlugin("react"), 69 | new webpack.PrefetchPlugin("react/lib/ReactComponentBrowserEnvironment") 70 | ]; 71 | if(options.prerender) { 72 | plugins.push(new StatsPlugin(path.join(__dirname, "build", "stats.prerender.json"), { 73 | chunkModules: true, 74 | exclude: excludeFromStats 75 | })); 76 | aliasLoader["react-proxy$"] = "react-proxy/unavailable"; 77 | aliasLoader["react-proxy-loader$"] = "react-proxy-loader/unavailable"; 78 | externals.push( 79 | /^react(\/.*)?$/, 80 | /^reflux(\/.*)?$/, 81 | "superagent", 82 | "async" 83 | ); 84 | plugins.push(new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); 85 | } else { 86 | plugins.push(new StatsPlugin(path.join(__dirname, "build", "stats.json"), { 87 | chunkModules: true, 88 | exclude: excludeFromStats 89 | })); 90 | } 91 | if(options.commonsChunk) { 92 | plugins.push(new webpack.optimize.CommonsChunkPlugin("commons", "commons.js" + (options.longTermCaching && !options.prerender ? "?[chunkhash]" : ""))); 93 | } 94 | var asyncLoader = { 95 | test: require("./app/route-handlers/async").map(function(name) { 96 | return path.join(__dirname, "app", "route-handlers", name); 97 | }), 98 | loader: options.prerender ? "react-proxy-loader/unavailable" : "react-proxy-loader" 99 | }; 100 | 101 | 102 | 103 | Object.keys(stylesheetLoaders).forEach(function(ext) { 104 | var stylesheetLoader = stylesheetLoaders[ext]; 105 | if(Array.isArray(stylesheetLoader)) stylesheetLoader = stylesheetLoader.join("!"); 106 | if(options.prerender) { 107 | stylesheetLoaders[ext] = stylesheetLoader.replace(/^css-loader/, "css-loader/locals"); 108 | } else if(options.separateStylesheet) { 109 | stylesheetLoaders[ext] = ExtractTextPlugin.extract("style-loader", stylesheetLoader); 110 | } else { 111 | stylesheetLoaders[ext] = "style-loader!" + stylesheetLoader; 112 | } 113 | }); 114 | if(options.separateStylesheet && !options.prerender) { 115 | plugins.push(new ExtractTextPlugin("[name].css" + (options.longTermCaching ? "?[contenthash]" : ""))); 116 | } 117 | if(options.minimize && !options.prerender) { 118 | plugins.push( 119 | new webpack.optimize.UglifyJsPlugin({ 120 | compressor: { 121 | warnings: false 122 | } 123 | }), 124 | new webpack.optimize.DedupePlugin() 125 | ); 126 | } 127 | if(options.minimize) { 128 | plugins.push( 129 | new webpack.DefinePlugin({ 130 | "process.env": { 131 | NODE_ENV: JSON.stringify("production") 132 | } 133 | }), 134 | new webpack.NoErrorsPlugin() 135 | ); 136 | } 137 | 138 | return { 139 | entry: entry, 140 | output: output, 141 | target: options.prerender ? "node" : "web", 142 | module: { 143 | loaders: [asyncLoader].concat(loadersByExtension(loaders)).concat(loadersByExtension(stylesheetLoaders)).concat(additionalLoaders) 144 | }, 145 | devtool: options.devtool, 146 | debug: options.debug, 147 | resolveLoader: { 148 | root: path.join(__dirname, "node_modules"), 149 | alias: aliasLoader 150 | }, 151 | externals: externals, 152 | resolve: { 153 | root: root, 154 | modulesDirectories: modulesDirectories, 155 | extensions: extensions, 156 | alias: alias 157 | }, 158 | plugins: plugins, 159 | devServer: { 160 | stats: { 161 | cached: false, 162 | exclude: excludeFromStats 163 | } 164 | } 165 | }; 166 | }; 167 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-starter", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "app/app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev-server": "webpack-dev-server --config webpack-dev-server.config.js --progress --colors --port 2992 --inline", 9 | "hot-dev-server": "webpack-dev-server --config webpack-hot-dev-server.config.js --hot --progress --colors --port 2992 --inline", 10 | "build": "webpack --config webpack-production.config.js --progress --profile --colors", 11 | "start-dev": "node lib/server-development", 12 | "start": "node lib/server-production" 13 | }, 14 | "keywords": [ 15 | "webpack", 16 | "react", 17 | "starter", 18 | "boilerplate", 19 | "hot" 20 | ], 21 | "author": "Tobias Koppers @sokra", 22 | "license": "MIT", 23 | "dependencies": { 24 | "async": "^1.2.0", 25 | "babel-core": "^5.0.12", 26 | "babel-loader": "^5.0.0", 27 | "body-parser": "^1.9.3", 28 | "css-loader": "^0.14.0", 29 | "express": "^4.7.2", 30 | "extract-text-webpack-plugin": "^0.8.0", 31 | "file-loader": "^0.8.1", 32 | "html-loader": "^0.3.0", 33 | "items-store": "^0.7.0", 34 | "less": "^2.1.2", 35 | "less-loader": "^2.0.0", 36 | "markdown-loader": "^0.1.2", 37 | "null-loader": "^0.1.0", 38 | "react": "^0.13.1", 39 | "react-hot-loader": "^1.0.0", 40 | "react-proxy-loader": "^0.3.1", 41 | "react-router": "^0.13.2", 42 | "stats-webpack-plugin": "^0.1.2", 43 | "style-loader": "^0.12.0", 44 | "superagent": "^1.1.0", 45 | "url-loader": "^0.5.5", 46 | "uuid": "^2.0.1", 47 | "webpack": "^1.8.5", 48 | "webpack-dev-server": "^1.4.7" 49 | }, 50 | "devDependencies": { 51 | "babel-eslint": "^3.0.1", 52 | "eslint": "^0.22.0", 53 | "eslint-plugin-react": "^2.2.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webpack/react-starter/16c0db3a3bad3cbf611007608339be31fc27bc8b/public/.empty -------------------------------------------------------------------------------- /webpack-dev-server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | devServer: true, 3 | devtool: "eval", 4 | debug: true 5 | }); 6 | -------------------------------------------------------------------------------- /webpack-hot-dev-server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | devServer: true, 3 | hotComponents: true, 4 | devtool: "eval", 5 | debug: true 6 | }); 7 | -------------------------------------------------------------------------------- /webpack-production.config.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require("./make-webpack-config")({ 3 | // commonsChunk: true, 4 | longTermCaching: true, 5 | separateStylesheet: true, 6 | minimize: true 7 | // devtool: "source-map" 8 | }), 9 | require("./make-webpack-config")({ 10 | prerender: true, 11 | minimize: true 12 | }) 13 | ]; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./make-webpack-config")({ 2 | 3 | }); --------------------------------------------------------------------------------