diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c380e58 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs. +# Requires EditorConfig JetBrains Plugin - http://github.com/editorconfig/editorconfig-jetbrains + +# Set this file as the topmost .editorconfig +# (multiple files can be used, and are applied starting from current document location) +root = true + +[{package.json}] +indent_style = space +indent_size = 2 + +# Use bracketed regexp to target specific file types or file locations +[*.{js,json}] + +# Use hard or soft tabs ["tab", "space"] +indent_style = space + +# Size of a single indent [an integer, "tab"] +indent_size = tab + +# Number of columns representing a tab character [an integer] +tab_width = 4 + +# Line breaks representation ["lf", "cr", "crlf"] +end_of_line = lf + +# ["latin1", "utf-8", "utf-16be", "utf-16le"] +charset = utf-8 + +# Remove any whitespace characters preceding newline characters ["true", "false"] +trim_trailing_whitespace = true + +# Ensure file ends with a newline when saving ["true", "false"] +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a62cfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +*.log +.DS_Store +lib +esdocs +coverage +build diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a38661a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +sudo: required +language: node_js +node_js: + - 4.2 + - stable +before_script: + - npm run link +after_success: + - npm run build diff --git a/LICENSE b/LICENSE index 9518836..ed56f05 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Roc +Copyright (c) 2016 VG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ff473e1..31af808 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# roc-package-web-app-react \ No newline at end of file +# roc-package-web-app-react +[![Build Status](https://travis-ci.org/rocjs/roc-package-web-app-react.svg?branch=master)](https://travis-ci.org/rocjs/roc-package-web-app-react) + +__Package for building React applications with Roc__ +- [roc-package-web-app-react](/packages/roc-package-web-app-react) +- [roc-package-web-app-react-dev](/packages/roc-package-web-app-react-dev) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..bb5f76d --- /dev/null +++ b/examples/README.md @@ -0,0 +1 @@ +Not all examples are fully updated to use the latest code, please be aware. diff --git a/examples/complex/config/default.json b/examples/complex/config/default.json new file mode 100755 index 0000000..6917947 --- /dev/null +++ b/examples/complex/config/default.json @@ -0,0 +1,9 @@ +{ + "DANGEROUSLY_EXPOSE_TO_CLIENT": { + "foo": "bar" + }, + "auth": { + "publicKey": "1", + "privateKey": "foobar" + } +} diff --git a/examples/complex/files/favicon.png b/examples/complex/files/favicon.png new file mode 100755 index 0000000..5341eb2 Binary files /dev/null and b/examples/complex/files/favicon.png differ diff --git a/examples/complex/files/test/index.html b/examples/complex/files/test/index.html new file mode 100755 index 0000000..e965047 --- /dev/null +++ b/examples/complex/files/test/index.html @@ -0,0 +1 @@ +Hello diff --git a/examples/complex/package.json b/examples/complex/package.json new file mode 100755 index 0000000..c878e57 --- /dev/null +++ b/examples/complex/package.json @@ -0,0 +1,16 @@ +{ + "name": "roc-web-react-complex-example", + "version": "1.0.0", + "description": "Roc Web React complex example", + "author": "VG", + "license": "MIT", + "dependencies": { + "redux-fetcher": "~1.0.1", + "redux-api-middleware": "vgno/redux-api-middleware#v1.0.0-beta5", + "roc-package-web-app-react": "*" + }, + "devDependencies": { + "roc-package-web-app-react-dev": "*", + "roc-plugin-style-sass": "*" + } +} diff --git a/examples/complex/roc.config.js b/examples/complex/roc.config.js new file mode 100755 index 0000000..cdc2dd7 --- /dev/null +++ b/examples/complex/roc.config.js @@ -0,0 +1,14 @@ +module.exports = { + settings: { + runtime: { + applicationName: 'My Roc Application', + serve: ['files', 'build/client'] + }, + build: { + koaMiddlewares: 'src/koa-middlewares.js', + reduxMiddlewares: 'src/middlewares.js', + reducers: 'src/reducers.js', + routes: 'src/routes.js' + } + } +}; diff --git a/examples/complex/src/components/about/index.js b/examples/complex/src/components/about/index.js new file mode 100755 index 0000000..012136b --- /dev/null +++ b/examples/complex/src/components/about/index.js @@ -0,0 +1,19 @@ +import React, { Component } from 'react'; +import { defer } from 'react-fetcher'; + +@defer(() => new Promise((resolve) => { + setTimeout(() => { + console.log('Completed!'); + resolve(); + }, 2000); +})) +export default class About extends Component { + render() { + return ( +
+

About us

+ +
+ ); + } +} diff --git a/examples/complex/src/components/app/index.js b/examples/complex/src/components/app/index.js new file mode 100755 index 0000000..f95fc25 --- /dev/null +++ b/examples/complex/src/components/app/index.js @@ -0,0 +1,22 @@ +import React, { Component } from 'react'; +import { IndexLink, Link } from 'react-router'; + +export default class App extends Component { + static propTypes = { + children: React.PropTypes.object + }; + + render() { + return ( +
+ + { this.props.children } +
+ ); + } +} diff --git a/examples/complex/src/components/bacon/index.js b/examples/complex/src/components/bacon/index.js new file mode 100755 index 0000000..924a64f --- /dev/null +++ b/examples/complex/src/components/bacon/index.js @@ -0,0 +1,68 @@ +import React, { Component } from 'react'; + +export default class Bacon extends Component { + render() { + /* eslint-disable max-len */ + return ( +
+

Bacon ipsum dolor

+

+ Bacon ipsum dolor amet hamburger swine filet mignon biltong shank, turkey alcatra brisket flank ribeye + landjaeger beef ribs. Ham flank pancetta biltong pork belly shankle brisket doner beef. Pig sirloin turkey + corned beef, alcatra biltong boudin pastrami. Tail pig pork filet mignon kevin chuck. Prosciutto ball tip + flank jerky ham, porchetta fatback kevin swine alcatra spare ribs pancetta. Pork belly tenderloin meatball + venison filet mignon andouille doner pork loin turducken strip steak. Shank andouille pig shankle. + + Drumstick shankle tri-tip pork chop salami bacon jowl. Shankle bacon tongue venison, brisket strip steak + cupim meatball. Flank turducken tenderloin rump pork belly ribeye. Drumstick tenderloin turkey, short loin + andouille meatloaf meatball brisket bresaola rump jowl. Drumstick tongue ball tip t-bone leberkas rump. + + Porchetta pastrami cow, short loin rump landjaeger brisket tongue beef bresaola pork chop drumstick. Swine + t-bone tongue pork belly ham turducken alcatra rump pork loin flank ribeye meatloaf capicola short ribs. + Flank ribeye shank, turkey pork chop tail tongue hamburger swine. Ham hock jowl meatloaf ham cow, rump + drumstick shankle flank hamburger fatback prosciutto biltong. + + Landjaeger bacon kevin sausage, tail bresaola shank alcatra pastrami jerky. Kielbasa salami landjaeger + ground round. Chicken ham brisket, boudin andouille corned beef jerky tri-tip short ribs kielbasa + landjaeger beef biltong jowl bacon. Pork shank leberkas, picanha beef ribs spare ribs cow beef drumstick + ball tip shankle short loin ground round pig. Picanha ground round venison pancetta drumstick ham biltong + bresaola salami sausage beef ribs boudin pork chop jerky. Brisket jowl chicken, kevin tongue beef + prosciutto meatloaf. + + Frankfurter meatball sausage turkey rump ham hock tongue doner leberkas drumstick jowl ground round filet + mignon. Tenderloin sirloin salami, shoulder ham landjaeger corned beef pastrami cow porchetta capicola + boudin tongue rump. Pork loin sirloin rump landjaeger drumstick pastrami frankfurter andouille doner + salami flank shoulder. Fatback bacon turducken frankfurter. Pancetta ground round flank pork, sausage pork + chop doner bacon ribeye shank tail filet mignon jowl swine short ribs. Jerky shank prosciutto ham hock + kevin picanha meatball short loin pork loin shankle ground round brisket pork belly. + + Chuck pig tri-tip, doner meatloaf rump ball tip tenderloin venison leberkas. Pork loin biltong t-bone + turducken. Jerky pork loin pork short ribs pastrami biltong, turkey meatball kevin sausage. Alcatra strip + steak corned beef, beef ribs andouille tenderloin biltong ribeye chuck. Doner chuck biltong venison pork + belly tail fatback cow t-bone short ribs ham hock jowl hamburger frankfurter meatball. + + Sausage beef ribs tail shoulder pork belly shank prosciutto pork tri-tip sirloin t-bone. Ground round spare + ribs turducken tongue. Shank tenderloin meatloaf, beef tongue beef ribs pastrami. Porchetta short ribs + sirloin, bacon drumstick prosciutto doner kevin pancetta tri-tip fatback. + + Meatloaf salami biltong shank, venison cow drumstick picanha capicola doner short loin. Doner filet mignon + biltong meatloaf. Filet mignon ribeye tail porchetta strip steak shoulder chicken short ribs. Strip steak + tenderloin pig filet mignon spare ribs, capicola pork loin prosciutto ground round leberkas tail chuck + porchetta biltong. Corned beef brisket frankfurter tongue capicola venison. Tail jowl ham venison. + + Venison spare ribs shank beef ribs sausage pork chop capicola jerky. Sirloin spare ribs ribeye strip steak + cow beef fatback brisket pork ball tip doner hamburger. Pork loin fatback swine kielbasa doner alcatra + salami porchetta drumstick tongue ground round. Strip steak sausage sirloin rump shoulder t-bone. Meatloaf + corned beef sausage, chuck ground round short ribs porchetta tri-tip. Hamburger kielbasa cow, picanha + boudin capicola rump pastrami ball tip pork chop swine. + + Chuck landjaeger pork, pork loin shankle tri-tip pastrami flank kielbasa picanha drumstick cupim chicken + beef. Pork loin frankfurter short loin, pancetta cupim ribeye jerky turkey beef ribs tri-tip meatball + swine tail flank. Pastrami salami turkey, turducken ball tip venison meatloaf sirloin pork chop drumstick + short loin. Andouille sirloin pig, tongue pork chop pastrami meatball filet mignon beef ribs chuck fatback. +

+
+ ); + /* eslint-enable max-len */ + } +} diff --git a/examples/complex/src/components/clicker/index.js b/examples/complex/src/components/clicker/index.js new file mode 100755 index 0000000..920a076 --- /dev/null +++ b/examples/complex/src/components/clicker/index.js @@ -0,0 +1,19 @@ +import React, { Component } from 'react'; + +export default class Clicker extends Component { + static propTypes = { + clicker: React.PropTypes.number.isRequired, + click: React.PropTypes.func.isRequired + }; + + render() { + return ( +
+

Clicker

+
+ { this.props.clicker } +
+
+ ); + } +} diff --git a/examples/complex/src/components/errors/index.js b/examples/complex/src/components/errors/index.js new file mode 100755 index 0000000..0074cfb --- /dev/null +++ b/examples/complex/src/components/errors/index.js @@ -0,0 +1,31 @@ +import React, { Component } from 'react'; + +import ErrorItem from './item'; + +export default class Errors extends Component { + static defaultProps = { + errors: [] + }; + + static propTypes = { + errors: React.PropTypes.array, + resetErrors: React.PropTypes.func + }; + + render() { + const errorList = this.props.errors.map((error, i) => ( + + )); + + if (errorList.length > 0) { + return ( +
+ { errorList } + +
+ ); + } + + return false; + } +} diff --git a/examples/complex/src/components/errors/item.js b/examples/complex/src/components/errors/item.js new file mode 100755 index 0000000..58db796 --- /dev/null +++ b/examples/complex/src/components/errors/item.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; + +import styles from './style.css'; + +export default class ErrorItem extends Component { + static defaultProps = { + key: 0, + error: '' + }; + + static propTypes = { + key: React.PropTypes.number.isRequired, + error: React.PropTypes.string.isRequired + }; + + render() { + return ( +
+

Error

+

{ this.props.error }

+
+ ); + } +} diff --git a/examples/complex/src/components/errors/style.css b/examples/complex/src/components/errors/style.css new file mode 100755 index 0000000..872b3e1 --- /dev/null +++ b/examples/complex/src/components/errors/style.css @@ -0,0 +1,7 @@ +.error { + background: #ff0000; + color: #fff; + padding: 5px; + margin: 0 0 10px 0; + border-radius: 8px; +} diff --git a/examples/complex/src/components/main/actions.js b/examples/complex/src/components/main/actions.js new file mode 100755 index 0000000..c065e38 --- /dev/null +++ b/examples/complex/src/components/main/actions.js @@ -0,0 +1,5 @@ +export function resetErrors() { + return { + type: 'RESET_ERROR_MESSAGES' + }; +} diff --git a/examples/complex/src/components/main/index.js b/examples/complex/src/components/main/index.js new file mode 100755 index 0000000..7cb0b7d --- /dev/null +++ b/examples/complex/src/components/main/index.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { prefetch } from 'react-fetcher'; + +// components +import Weather from '../weather'; +import Clicker from '../clicker'; +import Bacon from '../bacon'; +import Errors from '../errors'; + +// this generates fetch actions +import { createFetchAction } from 'redux-fetcher' + +// roc error action +import { resetErrors } from './actions'; + +// clicker reducer +import { click } from '../../reducers/clicker'; + +// util +import { prefetchWeather, mergeWeatherProps } from './util'; + +import styles from './style.css'; + +// this maps values from redux store to props of this component +function mapStateToProps(state) { + return { + clicker: state.clicker, + weather: state.weather, + errors: state.errors + }; +} + +// this maps action creators to dispatch, available as props on component +function mapDispatchToProps(dispatch) { + return bindActionCreators({ click, resetErrors, createFetchAction }, dispatch); +} + +// prefetch triggers on both server and client +@prefetch(prefetchWeather) +// mergeWeatherProps enriches dispatch props with weatherForceFetch +@connect(mapStateToProps, mapDispatchToProps, mergeWeatherProps) +export default class Main extends React.Component { + static propTypes = { + // bound actions + click: React.PropTypes.func.isRequired, + resetErrors: React.PropTypes.func.isRequired, + createFetchAction: React.PropTypes.func.isRequired, + weatherForceFetch: React.PropTypes.func.isRequired, + // connected values from store + clicker: React.PropTypes.number, + weather: React.PropTypes.object, + errors: React.PropTypes.array + }; + + render() { + return ( +
+ + + + + +
+ ); + } +} diff --git a/examples/complex/src/components/main/style.css b/examples/complex/src/components/main/style.css new file mode 100755 index 0000000..63ddd88 --- /dev/null +++ b/examples/complex/src/components/main/style.css @@ -0,0 +1,6 @@ +.main { + background: #fff; + width: 700px; + margin: auto; + padding: 20px; +} diff --git a/examples/complex/src/components/main/util.js b/examples/complex/src/components/main/util.js new file mode 100755 index 0000000..561470e --- /dev/null +++ b/examples/complex/src/components/main/util.js @@ -0,0 +1,34 @@ +import { createFetchAction } from 'redux-fetcher'; +import { FETCH_WEATHER } from '../../fetch'; + +// construct a weathermap api URL for the given location name +export function buildWeatherUrl(location) { + const query = location && encodeURIComponent(location) || 'moscow'; + return 'http://api.openweathermap.org/data/2.5/forecast/daily?q=' + + query + + '&appid=44db6a862fba0b067b1930da0d769e98'; +} + +// compatible with redux-fetcher '@prefetch' +export function prefetchWeather({ dispatch, location }) { + // fetch creates a redux action for us that will trigger + // respective types WEATHER_FETCH_SUCCESS, WEATHER_FETCH_PENDING or WEATHER_FETCH_FAILURE + const weatherAction = createFetchAction(FETCH_WEATHER, buildWeatherUrl(location.query.q)); + // dispatch the action + return dispatch(weatherAction); +} + +// compatible with react-redux third param 'mergeProps' +export function mergeWeatherProps(stateProps, dispatchProps, ownProps) { + // enrich dispatch props with a weather forced fetch, typically used for buttons + const newDispatchProps = { + ...dispatchProps, + weatherForceFetch: dispatchProps.createFetchAction.bind( + undefined, + FETCH_WEATHER, + buildWeatherUrl(ownProps.location.query.q), + { force: true, method: 'GET' } + ) + }; + return Object.assign({}, ownProps, stateProps, newDispatchProps); +} diff --git a/examples/complex/src/components/weather/button.js b/examples/complex/src/components/weather/button.js new file mode 100755 index 0000000..224e6c6 --- /dev/null +++ b/examples/complex/src/components/weather/button.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react'; + +export default class WeatherUpdateButton extends Component { + static propTypes = { + text: React.PropTypes.string, + onClick: React.PropTypes.func + }; + + render() { + return ( + + ); + } +} diff --git a/examples/complex/src/components/weather/data.js b/examples/complex/src/components/weather/data.js new file mode 100755 index 0000000..dfbfaae --- /dev/null +++ b/examples/complex/src/components/weather/data.js @@ -0,0 +1,25 @@ +import React, { Component } from 'react'; + +import styles from './style.css'; + +export default class WeatherData extends Component { + static propTypes = { + city: React.PropTypes.object, + list: React.PropTypes.array + }; + + render() { + return ( +
+

Fetched weather data

+ City name: { this.props.city.name }
+ Country: { this.props.city.country }
+ +

Weather data:

+
+ { JSON.stringify(this.props.list) } +
+
+ ); + } +} diff --git a/examples/complex/src/components/weather/error.js b/examples/complex/src/components/weather/error.js new file mode 100755 index 0000000..260aac9 --- /dev/null +++ b/examples/complex/src/components/weather/error.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react'; + +export default class WeatherError extends Component { + static propTypes = { + error: React.PropTypes.string + }; + + render() { + return ( +
+

Error loading weather data.

+ { this.props.error } +
+ ); + } +} diff --git a/examples/complex/src/components/weather/index.js b/examples/complex/src/components/weather/index.js new file mode 100755 index 0000000..744c994 --- /dev/null +++ b/examples/complex/src/components/weather/index.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; + +import styles from './style.css'; + +import WeatherLoader from './loader'; +import WeatherError from './error'; +import WeatherUpdateButton from './button'; +import WeatherData from './data'; + +export default class Weather extends Component { + static propTypes = { + payload: React.PropTypes.object, + loading: React.PropTypes.bool, + endpoint: React.PropTypes.string, + fetchWeatherData: React.PropTypes.func, + error: React.PropTypes.bool + }; + + render() { + const error = this.props.error; + + if (error) { + return ( +
+ + +
+ ); + } + + if (this.props.loading) { + return ( + + ); + } + + if (!this.props.payload && !this.props.loading) { + return ( +
+ No data provided + +
+ ); + } + + return ( +
+ } + +
+ ); + } +} diff --git a/examples/complex/src/components/weather/loader.js b/examples/complex/src/components/weather/loader.js new file mode 100755 index 0000000..744a8a4 --- /dev/null +++ b/examples/complex/src/components/weather/loader.js @@ -0,0 +1,17 @@ +import React, { Component } from 'react'; + +export default class WeatherLoader extends Component { + static propTypes = { + endpoint: React.PropTypes.string + }; + + render() { + const source = this.props.endpoint ? ` from ${this.props.endpoint}` : ''; + + return ( +
+ { `Loading weather data${source}...` } +
+ ); + } +} diff --git a/examples/complex/src/components/weather/style.css b/examples/complex/src/components/weather/style.css new file mode 100755 index 0000000..13fb349 --- /dev/null +++ b/examples/complex/src/components/weather/style.css @@ -0,0 +1,7 @@ +.weather { + margin: 20px 0 20px 0; +} + +.data { + font-size: 12px; +} diff --git a/examples/complex/src/fetch.js b/examples/complex/src/fetch.js new file mode 100755 index 0000000..280768c --- /dev/null +++ b/examples/complex/src/fetch.js @@ -0,0 +1 @@ +export const FETCH_WEATHER = 'weather'; diff --git a/examples/complex/src/koa-middlewares.js b/examples/complex/src/koa-middlewares.js new file mode 100755 index 0000000..a419b4f --- /dev/null +++ b/examples/complex/src/koa-middlewares.js @@ -0,0 +1,21 @@ +// Used as a silly example how a Koa middlware can acces the Redux store. +// If we have a querystring then counter will on the server be incremented. + +// Important that the middlewares yield next for everyting to work correct +export default function middlewares() { + return [function* (next) { + if (this.querystring.length > 0) { + this.state.reduxStore.dispatch({ + type: 'CLICKED' + }); + } + yield next; + }, function* (next) { + if (this.querystring.length > 0) { + this.state.reduxStore.dispatch({ + type: 'CLICKED' + }); + } + yield next; + }]; +} diff --git a/examples/complex/src/middlewares.js b/examples/complex/src/middlewares.js new file mode 100755 index 0000000..3e0af51 --- /dev/null +++ b/examples/complex/src/middlewares.js @@ -0,0 +1,15 @@ +/* global __NODE__ __DEV__ */ + +export default function getMiddlewares() { + if (__NODE__) { + // Add server middlewares here + } else { + // Add client middlewares here + } + + if (__DEV__) { + // Add dev middlewares here + } + + return []; +} diff --git a/examples/complex/src/reducers.js b/examples/complex/src/reducers.js new file mode 100755 index 0000000..ed5866b --- /dev/null +++ b/examples/complex/src/reducers.js @@ -0,0 +1,5 @@ +import { createFetchReducer } from 'redux-fetcher'; + +import { FETCH_WEATHER } from './fetch'; +export clicker from './reducers/clicker'; +export const weather = createFetchReducer(FETCH_WEATHER); diff --git a/examples/complex/src/reducers/clicker.js b/examples/complex/src/reducers/clicker.js new file mode 100755 index 0000000..9629a2a --- /dev/null +++ b/examples/complex/src/reducers/clicker.js @@ -0,0 +1,13 @@ +const CLICKED = 'CLICKED'; + +export default function clicker(state = 0, action = {}) { + if (action.type === CLICKED) { + return state + 1; + } + + return state; +} + +export function click() { + return { type: CLICKED }; +} diff --git a/examples/complex/src/routes.js b/examples/complex/src/routes.js new file mode 100755 index 0000000..c93c279 --- /dev/null +++ b/examples/complex/src/routes.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { Route, IndexRoute } from 'react-router'; + +import App from './components/app'; +import Main from './components/main'; +import About from './components/about'; + +export default () => ( + + + + +); diff --git a/examples/realtime-redux/components/stock-event-list/index.js b/examples/realtime-redux/components/stock-event-list/index.js new file mode 100755 index 0000000..f90dd3f --- /dev/null +++ b/examples/realtime-redux/components/stock-event-list/index.js @@ -0,0 +1,35 @@ +import React from 'react'; + +import styles from './style.css'; +import StockEventView from '../stock-event'; + +export default class StockEventListView extends React.Component { + static propTypes = { + numToDisplay: React.PropTypes.number, + events: React.PropTypes.array.isRequired + }; + + static defaultProps = { + numToDisplay: 7, + events: [] + }; + + render() { + const events = this.props.events.map((event) => { + return ( + + ); + }); + + return ( +
+ { events } +
+ ); + } +} diff --git a/examples/realtime-redux/components/stock-event-list/style.css b/examples/realtime-redux/components/stock-event-list/style.css new file mode 100755 index 0000000..061f601 --- /dev/null +++ b/examples/realtime-redux/components/stock-event-list/style.css @@ -0,0 +1,4 @@ +.list { + width: 250px; + float: right; +} diff --git a/examples/realtime-redux/components/stock-event/index.js b/examples/realtime-redux/components/stock-event/index.js new file mode 100755 index 0000000..a933d3d --- /dev/null +++ b/examples/realtime-redux/components/stock-event/index.js @@ -0,0 +1,35 @@ +import React from 'react'; + +import styles from './style.scss'; + +export default class StockEventView extends React.Component { + static propTypes = { + name: React.PropTypes.string.isRequired, + current: React.PropTypes.number.isRequired, + diff: React.PropTypes.number.isRequired + }; + + static defaultProps = { + name: '', + current: 0, + diff: 0 + }; + + render() { + const diffClass = this.props.diff < 0 ? styles.down : styles.up; + + return ( +
+
+ { this.props.name } +
+
+ { this.props.current } +
+
+ { this.props.diff } +
+
+ ); + } +} diff --git a/examples/realtime-redux/components/stock-event/style.scss b/examples/realtime-redux/components/stock-event/style.scss new file mode 100755 index 0000000..264b102 --- /dev/null +++ b/examples/realtime-redux/components/stock-event/style.scss @@ -0,0 +1,26 @@ +.event { + text-align: center; + font-size: 12px; + border: 1px solid; + border-radius: 4px; + padding: 10px; + margin: 0 0 4px 0; + background: rgba(151,187,205, 0.7); + animation: fadein 0.5s; + height: 65px; + @keyframes fadein { + from { font-size: 0; padding: 0; height: 5px; } + } +} + +.name { + font-weight: bold; +} + +.up { + color: green; +} + +.down { + color: red; +} diff --git a/examples/realtime-redux/components/stock-graph-list/index.js b/examples/realtime-redux/components/stock-graph-list/index.js new file mode 100755 index 0000000..9ce0bbb --- /dev/null +++ b/examples/realtime-redux/components/stock-graph-list/index.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import styles from './style.css'; +import StockGraphView from '../stock-graph'; + +export default class StockGraphList extends React.Component { + static propTypes = { + graphs: React.PropTypes.object.isRequired + }; + + static defaultProps = { + graphs: {} + }; + + render() { + const graphs = Object.keys(this.props.graphs).map((e) => { + const graph = this.props.graphs[e]; + return ; + }); + + return ( +
+

SSE Graphed Events

+ { graphs } +
+ ); + } +} diff --git a/examples/realtime-redux/components/stock-graph-list/style.css b/examples/realtime-redux/components/stock-graph-list/style.css new file mode 100755 index 0000000..f0c121c --- /dev/null +++ b/examples/realtime-redux/components/stock-graph-list/style.css @@ -0,0 +1,4 @@ +.graphs { + width: 500px; + float: left; +} diff --git a/examples/realtime-redux/components/stock-graph/index.js b/examples/realtime-redux/components/stock-graph/index.js new file mode 100755 index 0000000..bdc2fd5 --- /dev/null +++ b/examples/realtime-redux/components/stock-graph/index.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { Line as LineChart } from 'react-chartjs'; + +import styles from './style.css'; + +export default class StockGraphView extends React.Component { + static propTypes = { + points: React.PropTypes.number.isRequired, + data: React.PropTypes.object.isRequired + }; + + static defaultProps = { + points: 15, + data: { + LONG_NAME: 'Loading...', + TICKER: 'Loading...', + LAST: '-', + CHANGE: '-', + times: [], + index: [] + } + }; + + render() { + const header = `${this.props.data.LONG_NAME} (${this.props.data.TICKER})`; + const chart = livePresentationView(this.props); + + return ( +
+

{ header }

+ Nå: {this.props.data.LAST}
+ Endring: {this.props.data.CHANGE}
+ { chart } +
+ ); + } +} + +const calculateChartData = (props) => { + return { + labels: props.data.times, + datasets: [ + { + label: 'Main Index', + fillColor: 'rgba(151,187,205,0.2)', + strokeColor: 'rgba(151,187,205,1)', + pointColor: 'rgba(151,187,205,1)', + pointStrokeColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStroke: 'rgba(151,187,205,1)', + data: props.data.index + } + ] + }; +}; + +const plotDataCollectComplete = (props) => props.data.index.length >= props.points; +const plotDataCollectProgress = (props) => Math.floor(props.data.index.length / props.points * 100) + '%'; + +const graphView = (props) => React.createElement(LineChart, { data: calculateChartData(props), width: 500 }); +const progressView = (props) => React.createElement('h3', null, 'Collecting Data: ' + plotDataCollectProgress(props)); +const livePresentationView = (props) => plotDataCollectComplete(props) ? graphView(props) : progressView(props); diff --git a/examples/realtime-redux/components/stock-graph/style.css b/examples/realtime-redux/components/stock-graph/style.css new file mode 100755 index 0000000..730caa7 --- /dev/null +++ b/examples/realtime-redux/components/stock-graph/style.css @@ -0,0 +1,3 @@ +.graph { + margin: 0 0 40px 0; +} diff --git a/examples/realtime-redux/components/stock/index.js b/examples/realtime-redux/components/stock/index.js new file mode 100755 index 0000000..d96e951 --- /dev/null +++ b/examples/realtime-redux/components/stock/index.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import StockEventListView from '../stock-event-list'; +import StockGraphListView from '../stock-graph-list'; +import styles from './style.css'; + +export default class StockView extends React.Component { + static propTypes = { + data: React.PropTypes.object.isRequired + }; + + static defaultProps = { + data: { + graphs: {}, + events: [] + } + }; + + render() { + return ( +
+ + +
+ ); + } +} diff --git a/examples/realtime-redux/components/stock/style.css b/examples/realtime-redux/components/stock/style.css new file mode 100755 index 0000000..27e051c --- /dev/null +++ b/examples/realtime-redux/components/stock/style.css @@ -0,0 +1,9 @@ +.stock { + position: relative; + background: #fff; + border: 1px solid; + font-size: 14px; + font-family: "Helvetica"; + padding: 10px; + height: 650px; +} diff --git a/examples/realtime-redux/config/custom-environment-variables.json b/examples/realtime-redux/config/custom-environment-variables.json new file mode 100755 index 0000000..a982260 --- /dev/null +++ b/examples/realtime-redux/config/custom-environment-variables.json @@ -0,0 +1,7 @@ +{ + "stock": { + "sseSource": "STOCK_SSE_SOURCE", + "restSource": "STOCK_REST_SOURCE", + "collect": "STOCK_COLLECT" + } +} diff --git a/examples/realtime-redux/config/default.json b/examples/realtime-redux/config/default.json new file mode 100755 index 0000000..fd726fc --- /dev/null +++ b/examples/realtime-redux/config/default.json @@ -0,0 +1,10 @@ +{ + "stock": { + "sseSource": "http://sse.e24.no/market", + "restSource": "http://bors.e24.no/e24/servlets/newt/json/instrument?ticker=", + "collect": [ + "OSEBX.OSE", + "C:PBROUSDBR\\SP.IDCENE" + ] + } +} diff --git a/examples/realtime-redux/main.js b/examples/realtime-redux/main.js new file mode 100755 index 0000000..2cfa875 --- /dev/null +++ b/examples/realtime-redux/main.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { prefetch } from 'react-fetcher'; + +import { appConfig } from 'roc-package-web-app-react/app/shared'; + +import { stockLoad, stockError } from './stock'; +import * as rest from './util/rest'; +import * as sse from './util/sse'; +import StockView from './components/stock'; +import styles from './style.scss'; + +function mapStateToProps(state) { + return { + data: state.stock.data, + errors: state.stock.errors + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ stockLoad, stockError }, dispatch); +} + +@prefetch(({ dispatch }) => { + const dataForServer = rest.fetchServerData(); + + if (dataForServer) { + return dataForServer + .then(rest.getTickerData) + .then(rest.createEventsFromTickerData) + .then(rest.handleEvents(dispatch, stockLoad, stockError)) + .catch(rest.handleFetchDataError(dispatch, stockError)); + } +}) +@connect(mapStateToProps, mapDispatchToProps) +export default class Stock extends React.Component { + static propTypes = { + data: React.PropTypes.object.isRequired, + errors: React.PropTypes.array, + stockLoad: React.PropTypes.func.isRequired, + stockError: React.PropTypes.func.isRequired + }; + + static defaultProps = { + data: {}, + errors: [] + }; + + // provides realtime datastream clientside + componentDidMount() { + this.sseSub = sse.subscribe(appConfig.sseSource, (stockEvent) => { + const data = JSON.parse(stockEvent.data); + this.props.stockLoad([data]); + }); + } + + componentWillUnmount() { + if (this.sseSub) { + this.sseSub.close(); + } + } + + render() { + const errors = this.props.errors.map((error, key) => { + return ( +

{error}

+ ); + }); + + return ( +
+
+ { errors.length > 0 ? errors : } +
+
+ ); + } +} diff --git a/examples/realtime-redux/package.json b/examples/realtime-redux/package.json new file mode 100755 index 0000000..51284a4 --- /dev/null +++ b/examples/realtime-redux/package.json @@ -0,0 +1,17 @@ +{ + "name": "roc-package-web-app-react-realtime-example", + "version": "1.0.0", + "description": "Roc Web React realtime example", + "author": "VG", + "license": "MIT", + "dependencies": { + "roc-package-web-app-react": "*", + "chart.js": "^1.0.2", + "isomorphic-fetch": "^2.2.0", + "react-chartjs": "^0.6.0" + }, + "devDependencies": { + "roc-package-web-app-react-dev": "*", + "roc-plugin-style-sass": "^1.0.0-alpha" + } +} diff --git a/examples/realtime-redux/reducers.js b/examples/realtime-redux/reducers.js new file mode 100755 index 0000000..498ad5c --- /dev/null +++ b/examples/realtime-redux/reducers.js @@ -0,0 +1 @@ +export stock from './stock'; diff --git a/examples/realtime-redux/roc.config.js b/examples/realtime-redux/roc.config.js new file mode 100755 index 0000000..f1a5b69 --- /dev/null +++ b/examples/realtime-redux/roc.config.js @@ -0,0 +1,8 @@ +module.exports = { + settings: { + runtime: { + applicationName: 'Stock live demo', + configWhitelistProperty: 'stock' + } + } +}; diff --git a/examples/realtime-redux/routes.js b/examples/realtime-redux/routes.js new file mode 100755 index 0000000..5ac07f3 --- /dev/null +++ b/examples/realtime-redux/routes.js @@ -0,0 +1,8 @@ +import React from 'react'; +import { Route, IndexRoute } from 'react-router'; + +import Main from './main'; + +export default () => ( + +); diff --git a/examples/realtime-redux/stock.js b/examples/realtime-redux/stock.js new file mode 100755 index 0000000..80dcc7f --- /dev/null +++ b/examples/realtime-redux/stock.js @@ -0,0 +1,105 @@ +import { appConfig } from 'roc-package-web-app-react/app/shared'; + +const STOCK_LOAD = 'STOCK_LOAD'; +const STOCK_REMOVE = 'STOCK_REMOVE'; +const STOCK_LOAD_ERROR = 'STOCK_LOAD_ERROR'; + +function createGraph(name, event, point) { + const index = [point]; + const times = []; + + for (let i = 1; i <= 15; i++) { + times.push(i); + } + + return { ...event, index, times }; +} + +function updateGraph(event, graph, point) { + const index = [...graph.index]; + const times = [...graph.times]; + + index.push(point); + + return { ...event, index: index.slice(-15), times }; +} + +export default function reducer( + state = { data: { graphs: {}, events: []}, errors: [] }, + action = { data: [], errors: [] } + ) { + if (action.type === STOCK_LOAD) { + // get copy of action data + const newEvents = [ ...action.data ]; + + // get copy of existing events + const events = [...state.data.events]; + + // get copy of current graph data + const graphs = {...state.data.graphs }; + + // the tickers we want to generate graph data for from configuration + const tickersToGraph = appConfig.collect; + + for (const event of newEvents) { + // build event key, used by react motion + event.key = event.TICKER + event.LAST + event.TIME; + + // ignore identical events + if (state.data.events.find(e => e.key === event.key)) { + continue; + } + + // add to events + events.unshift(event); + + // graph ticker if configured + if (tickersToGraph.indexOf(event.TICKER) > -1) { + const graph = graphs[event.TICKER]; + const graphPoint = parseFloat(event.LAST.toFixed(2)); + + if (graph) { + graphs[event.TICKER] = updateGraph(event, graph, graphPoint); + } else { + graphs[event.TICKER] = createGraph(event.TICKER, event, graphPoint); + } + } + } + + return { + ...state, + errors: [], + data: { + graphs, + events: events.slice(0,9) + } + }; + } else if (action.type === STOCK_LOAD_ERROR) { + const errors = [ ...action.errors ]; + const errorTexts = []; + + for (const error of errors) { + if (error.message) { + errorTexts.push(error.message); + } else { + errorTexts.push(error); + } + } + + return { + ...state, + data: {}, + errors: errorTexts + }; + } + + return state; +} + +export function stockLoad(data) { + return { type: STOCK_LOAD, data }; +} + +export function stockError(errors) { + return { type: STOCK_LOAD_ERROR, errors }; +} diff --git a/examples/realtime-redux/style.scss b/examples/realtime-redux/style.scss new file mode 100755 index 0000000..8c56c29 --- /dev/null +++ b/examples/realtime-redux/style.scss @@ -0,0 +1,5 @@ +.stock { + background: #fff; + width: 800px; + margin: auto; +} diff --git a/examples/realtime-redux/util/rest.js b/examples/realtime-redux/util/rest.js new file mode 100755 index 0000000..d246b2a --- /dev/null +++ b/examples/realtime-redux/util/rest.js @@ -0,0 +1,97 @@ +/* globals __NODE__ */ +import { appConfig } from 'roc-package-web-app-react/app/shared'; + +/** + * Promises to return data from configured rest endpoint + * + * @return {Promise} data + */ +export function fetchServerData() { + if (__NODE__) { + let fetchTickers = []; + const fetch = require('isomorphic-fetch'); + for (const ticker of appConfig.collect) { + const tickerUrl = appConfig.restSource + encodeURIComponent(ticker); + fetchTickers.push(fetch(tickerUrl)); + } + + return Promise.all(fetchTickers); + } +} + +/** + * Promises to extract all tickers from raw responses + * + * @return {Promise} tickers + */ +export function getTickerData(responses) { + const tickers = []; + + for (const response of responses) { + if (response.status < 400) { + tickers.push(response.json()); + } + } + + return Promise.all(tickers); +} + +/** Promises to create event objects from ticker data + * @param {array} tickersData + * + * @return {Promise} ticker data + */ +export function createEventsFromTickerData(tickersData) { + return new Promise((resolve) => { + const events = []; + + for (const tickerJson of tickersData) { + const eventJson = tickerJson[0]; + + events.push({ + TICKER: eventJson.it, + TIME: eventJson.tu, + LAST: eventJson.la, + CHANGE: eventJson.ch, + LONG_NAME: eventJson.ln + }); + } + resolve(events); + }); +} + +/** Event handling function using given dispatcher and handlers + * + * @param {function} dispatch + * @param {function} onSuccess + * @param {function} onError + * + * @return {function} eventHandler + */ +export function handleEvents(dispatch, onSuccess, onError) { + return (events) => { + return new Promise((resolve, reject) => { + if (events.length > 0) { + dispatch(onSuccess(events)); + return resolve(events.length); + } + + dispatch(onError(new Error('No events found'))); + return reject(0); + }); + }; +} + +/** Error handling function using given dispatcher and error handler + * + * @param {function} dispatch + * @param {function} onError + * + * @return {function} errorHandler + */ +export function handleFetchDataError(dispatch, onError) { + return (error) => { + const errorAction = onError([error]); + return dispatch(errorAction); + }; +} diff --git a/examples/realtime-redux/util/sse.js b/examples/realtime-redux/util/sse.js new file mode 100755 index 0000000..1a2c938 --- /dev/null +++ b/examples/realtime-redux/util/sse.js @@ -0,0 +1,16 @@ +export function subscribe(url, onMessage, onOpen, onError) { + if (!!window.EventSource) { + const source = new window.EventSource(url); + if (onMessage) { + source.addEventListener('message', onMessage); + } + if (onOpen) { + source.addEventListener('open', onOpen); + } + if (onError) { + source.addEventListener('error', onError); + } + + return source; + } +} diff --git a/examples/simple-redux/clicker.js b/examples/simple-redux/clicker.js new file mode 100755 index 0000000..9629a2a --- /dev/null +++ b/examples/simple-redux/clicker.js @@ -0,0 +1,13 @@ +const CLICKED = 'CLICKED'; + +export default function clicker(state = 0, action = {}) { + if (action.type === CLICKED) { + return state + 1; + } + + return state; +} + +export function click() { + return { type: CLICKED }; +} diff --git a/examples/simple-redux/main.js b/examples/simple-redux/main.js new file mode 100755 index 0000000..52c2afe --- /dev/null +++ b/examples/simple-redux/main.js @@ -0,0 +1,91 @@ +import React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import { click } from './clicker'; + +import styles from './style.scss'; + +function mapStateToProps(state) { + return { + clicker: state.clicker + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ click }, dispatch); +} + +@connect(mapStateToProps, mapDispatchToProps) +export default class Main extends React.Component { + static propTypes = { + clicker: React.PropTypes.number.isRequired, + click: React.PropTypes.func.isRequired + }; + + render() { + return ( +
+

Bacon ipsum dolor

+
+ { this.props.clicker } +
+

+ Bacon ipsum dolor amet hamburger swine filet mignon biltong shank, turkey alcatra brisket flank ribeye + landjaeger beef ribs. Ham flank pancetta biltong pork belly shankle brisket doner beef. Pig sirloin turkey + corned beef, alcatra biltong boudin pastrami. Tail pig pork filet mignon kevin chuck. Prosciutto ball tip + flank jerky ham, porchetta fatback kevin swine alcatra spare ribs pancetta. Pork belly tenderloin meatball + venison filet mignon andouille doner pork loin turducken strip steak. Shank andouille pig shankle. + + Drumstick shankle tri-tip pork chop salami bacon jowl. Shankle bacon tongue venison, brisket strip steak + cupim meatball. Flank turducken tenderloin rump pork belly ribeye. Drumstick tenderloin turkey, short loin + andouille meatloaf meatball brisket bresaola rump jowl. Drumstick tongue ball tip t-bone leberkas rump. + + Porchetta pastrami cow, short loin rump landjaeger brisket tongue beef bresaola pork chop drumstick. Swine + t-bone tongue pork belly ham turducken alcatra rump pork loin flank ribeye meatloaf capicola short ribs. + Flank ribeye shank, turkey pork chop tail tongue hamburger swine. Ham hock jowl meatloaf ham cow, rump + drumstick shankle flank hamburger fatback prosciutto biltong. + + Landjaeger bacon kevin sausage, tail bresaola shank alcatra pastrami jerky. Kielbasa salami landjaeger + ground round. Chicken ham brisket, boudin andouille corned beef jerky tri-tip short ribs kielbasa + landjaeger beef biltong jowl bacon. Pork shank leberkas, picanha beef ribs spare ribs cow beef drumstick + ball tip shankle short loin ground round pig. Picanha ground round venison pancetta drumstick ham biltong + bresaola salami sausage beef ribs boudin pork chop jerky. Brisket jowl chicken, kevin tongue beef + prosciutto meatloaf. + + Frankfurter meatball sausage turkey rump ham hock tongue doner leberkas drumstick jowl ground round filet + mignon. Tenderloin sirloin salami, shoulder ham landjaeger corned beef pastrami cow porchetta capicola + boudin tongue rump. Pork loin sirloin rump landjaeger drumstick pastrami frankfurter andouille doner + salami flank shoulder. Fatback bacon turducken frankfurter. Pancetta ground round flank pork, sausage pork + chop doner bacon ribeye shank tail filet mignon jowl swine short ribs. Jerky shank prosciutto ham hock + kevin picanha meatball short loin pork loin shankle ground round brisket pork belly. + + Chuck pig tri-tip, doner meatloaf rump ball tip tenderloin venison leberkas. Pork loin biltong t-bone + turducken. Jerky pork loin pork short ribs pastrami biltong, turkey meatball kevin sausage. Alcatra strip + steak corned beef, beef ribs andouille tenderloin biltong ribeye chuck. Doner chuck biltong venison pork + belly tail fatback cow t-bone short ribs ham hock jowl hamburger frankfurter meatball. + + Sausage beef ribs tail shoulder pork belly shank prosciutto pork tri-tip sirloin t-bone. Ground round spare + ribs turducken tongue. Shank tenderloin meatloaf, beef tongue beef ribs pastrami. Porchetta short ribs + sirloin, bacon drumstick prosciutto doner kevin pancetta tri-tip fatback. + + Meatloaf salami biltong shank, venison cow drumstick picanha capicola doner short loin. Doner filet mignon + biltong meatloaf. Filet mignon ribeye tail porchetta strip steak shoulder chicken short ribs. Strip steak + tenderloin pig filet mignon spare ribs, capicola pork loin prosciutto ground round leberkas tail chuck + porchetta biltong. Corned beef brisket frankfurter tongue capicola venison. Tail jowl ham venison. + + Venison spare ribs shank beef ribs sausage pork chop capicola jerky. Sirloin spare ribs ribeye strip steak + cow beef fatback brisket pork ball tip doner hamburger. Pork loin fatback swine kielbasa doner alcatra + salami porchetta drumstick tongue ground round. Strip steak sausage sirloin rump shoulder t-bone. Meatloaf + corned beef sausage, chuck ground round short ribs porchetta tri-tip. Hamburger kielbasa cow, picanha + boudin capicola rump pastrami ball tip pork chop swine. + + Chuck landjaeger pork, pork loin shankle tri-tip pastrami flank kielbasa picanha drumstick cupim chicken + beef. Pork loin frankfurter short loin, pancetta cupim ribeye jerky turkey beef ribs tri-tip meatball + swine tail flank. Pastrami salami turkey, turducken ball tip venison meatloaf sirloin pork chop drumstick + short loin. Andouille sirloin pig, tongue pork chop pastrami meatball filet mignon beef ribs chuck fatback. +

+
+ ); + } +} diff --git a/examples/simple-redux/package.json b/examples/simple-redux/package.json new file mode 100755 index 0000000..f3b02a7 --- /dev/null +++ b/examples/simple-redux/package.json @@ -0,0 +1,14 @@ +{ + "name": "roc-web-react-simple-redux-example", + "version": "1.0.0", + "description": "Roc Web React simple redux example", + "author": "VG", + "license": "MIT", + "dependencies": { + "roc-package-web-app-react": "*" + }, + "devDependencies": { + "roc-package-web-app-react-dev": "*", + "roc-plugin-style-sass": "*" + } +} diff --git a/examples/simple-redux/reducers.js b/examples/simple-redux/reducers.js new file mode 100755 index 0000000..5820ad1 --- /dev/null +++ b/examples/simple-redux/reducers.js @@ -0,0 +1 @@ +export clicker from './clicker'; diff --git a/examples/simple-redux/routes.js b/examples/simple-redux/routes.js new file mode 100755 index 0000000..5ac07f3 --- /dev/null +++ b/examples/simple-redux/routes.js @@ -0,0 +1,8 @@ +import React from 'react'; +import { Route, IndexRoute } from 'react-router'; + +import Main from './main'; + +export default () => ( + +); diff --git a/examples/simple-redux/style.scss b/examples/simple-redux/style.scss new file mode 100755 index 0000000..63ddd88 --- /dev/null +++ b/examples/simple-redux/style.scss @@ -0,0 +1,6 @@ +.main { + background: #fff; + width: 700px; + margin: auto; + padding: 20px; +} diff --git a/examples/simple/main.js b/examples/simple/main.js new file mode 100755 index 0000000..d0e3413 --- /dev/null +++ b/examples/simple/main.js @@ -0,0 +1,70 @@ +import React from 'react'; + +import styles from './style.css'; + +export default class Main extends React.Component { + render() { + //const image = require('./roc.png'); + + return ( +
+

Bacon ipsum dolor?

+

+ Bacon ipsum dolor amet hamburger swine filet mignon biltong shank, turkey alcatra brisket flank ribeye + landjaeger beef ribs. Ham flank pancetta biltong pork belly shankle brisket doner beef. Pig sirloin turkey + corned beef, alcatra biltong boudin pastrami. Tail pig pork filet mignon kevin chuck. Prosciutto ball tip + flank jerky ham, porchetta fatback kevin swine alcatra spare ribs pancetta. Pork belly tenderloin meatball + venison filet mignon andouille doner pork loin turducken strip steak. Shank andouille pig shankle. + + Drumstick shankle tri-tip pork chop salami bacon jowl. Shankle bacon tongue venison, brisket strip steak + cupim meatball. Flank turducken tenderloin rump pork belly ribeye. Drumstick tenderloin turkey, short loin + andouille meatloaf meatball brisket bresaola rump jowl. Drumstick tongue ball tip t-bone leberkas rump. + + Porchetta pastrami cow, short loin rump landjaeger brisket tongue beef bresaola pork chop drumstick. Swine + t-bone tongue pork belly ham turducken alcatra rump pork loin flank ribeye meatloaf capicola short ribs. + Flank ribeye shank, turkey pork chop tail tongue hamburger swine. Ham hock jowl meatloaf ham cow, rump + drumstick shankle flank hamburger fatback prosciutto biltong. + + Landjaeger bacon kevin sausage, tail bresaola shank alcatra pastrami jerky. Kielbasa salami landjaeger + ground round. Chicken ham brisket, boudin andouille corned beef jerky tri-tip short ribs kielbasa + landjaeger beef biltong jowl bacon. Pork shank leberkas, picanha beef ribs spare ribs cow beef drumstick + ball tip shankle short loin ground round pig. Picanha ground round venison pancetta drumstick ham biltong + bresaola salami sausage beef ribs boudin pork chop jerky. Brisket jowl chicken, kevin tongue beef + prosciutto meatloaf. + + Frankfurter meatball sausage turkey rump ham hock tongue doner leberkas drumstick jowl ground round filet + mignon. Tenderloin sirloin salami, shoulder ham landjaeger corned beef pastrami cow porchetta capicola + boudin tongue rump. Pork loin sirloin rump landjaeger drumstick pastrami frankfurter andouille doner + salami flank shoulder. Fatback bacon turducken frankfurter. Pancetta ground round flank pork, sausage pork + chop doner bacon ribeye shank tail filet mignon jowl swine short ribs. Jerky shank prosciutto ham hock + kevin picanha meatball short loin pork loin shankle ground round brisket pork belly. + + Chuck pig tri-tip, doner meatloaf rump ball tip tenderloin venison leberkas. Pork loin biltong t-bone + turducken. Jerky pork loin pork short ribs pastrami biltong, turkey meatball kevin sausage. Alcatra strip + steak corned beef, beef ribs andouille tenderloin biltong ribeye chuck. Doner chuck biltong venison pork + belly tail fatback cow t-bone short ribs ham hock jowl hamburger frankfurter meatball. + + Sausage beef ribs tail shoulder pork belly shank prosciutto pork tri-tip sirloin t-bone. Ground round spare + ribs turducken tongue. Shank tenderloin meatloaf, beef tongue beef ribs pastrami. Porchetta short ribs + sirloin, bacon drumstick prosciutto doner kevin pancetta tri-tip fatback. + + Meatloaf salami biltong shank, venison cow drumstick picanha capicola doner short loin. Doner filet mignon + biltong meatloaf. Filet mignon ribeye tail porchetta strip steak shoulder chicken short ribs. Strip steak + tenderloin pig filet mignon spare ribs, capicola pork loin prosciutto ground round leberkas tail chuck + porchetta biltong. Corned beef brisket frankfurter tongue capicola venison. Tail jowl ham venison. + + Venison spare ribs shank beef ribs sausage pork chop capicola jerky. Sirloin spare ribs ribeye strip steak + cow beef fatback brisket pork ball tip doner hamburger. Pork loin fatback swine kielbasa doner alcatra + salami porchetta drumstick tongue ground round. Strip steak sausage sirloin rump shoulder t-bone. Meatloaf + corned beef sausage, chuck ground round short ribs porchetta tri-tip. Hamburger kielbasa cow, picanha + boudin capicola rump pastrami ball tip pork chop swine. + + Chuck landjaeger pork, pork loin shankle tri-tip pastrami flank kielbasa picanha drumstick cupim chicken + beef. Pork loin frankfurter short loin, pancetta cupim ribeye jerky turkey beef ribs tri-tip meatball + swine tail flank. Pastrami salami turkey, turducken ball tip venison meatloaf sirloin pork chop drumstick + short loin. Andouille sirloin pig, tongue pork chop pastrami meatball filet mignon beef ribs chuck fatback. +

+
+ ); + } +} diff --git a/examples/simple/package.json b/examples/simple/package.json new file mode 100755 index 0000000..e980f83 --- /dev/null +++ b/examples/simple/package.json @@ -0,0 +1,16 @@ +{ + "name": "roc-web-react-simple-example", + "version": "1.0.0", + "description": "Roc Web React simple example", + "author": "VG", + "license": "MIT", + "scripts": { + "dev": "roc dev --applicationName 'Simple Example'" + }, + "dependencies": { + "roc-package-web-app-react": "*" + }, + "devDependencies": { + "roc-package-web-app-react-dev": "*" + } +} diff --git a/examples/simple/roc.png b/examples/simple/roc.png new file mode 100755 index 0000000..5341eb2 Binary files /dev/null and b/examples/simple/roc.png differ diff --git a/examples/simple/routes.js b/examples/simple/routes.js new file mode 100755 index 0000000..5ac07f3 --- /dev/null +++ b/examples/simple/routes.js @@ -0,0 +1,8 @@ +import React from 'react'; +import { Route, IndexRoute } from 'react-router'; + +import Main from './main'; + +export default () => ( + +); diff --git a/examples/simple/style.css b/examples/simple/style.css new file mode 100755 index 0000000..63ddd88 --- /dev/null +++ b/examples/simple/style.css @@ -0,0 +1,6 @@ +.main { + background: #fff; + width: 700px; + margin: auto; + padding: 20px; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b69aae --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "roc-package-web-app-react", + "private": true, + "license": "MIT", + "scripts": { + "rid": "rid", + "build": "rid build", + "lint": "rid lint:alias", + "link": "rid link", + "test": "npm run lint" + }, + "devDependencies": { + "@rocjs/roc-internal-dev": "^1.0.0" + } +} diff --git a/packages/roc-package-web-app-react-dev/.eslintignore b/packages/roc-package-web-app-react-dev/.eslintignore new file mode 100644 index 0000000..bc73336 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/.eslintignore @@ -0,0 +1,3 @@ +lib +esdocs +docs diff --git a/packages/roc-package-web-app-react-dev/.eslintrc b/packages/roc-package-web-app-react-dev/.eslintrc new file mode 100644 index 0000000..811e6a8 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/.eslintrc @@ -0,0 +1,13 @@ +{ + "extends": "vgno", + + "parser": "babel-eslint", + + "env": { + "es6": true + }, + + "ecmaFeatures": { + "modules": true + } +} diff --git a/packages/roc-package-web-app-react-dev/README.md b/packages/roc-package-web-app-react-dev/README.md new file mode 100644 index 0000000..f37836c --- /dev/null +++ b/packages/roc-package-web-app-react-dev/README.md @@ -0,0 +1,11 @@ +# roc-package-web-app-react-dev +Package for building React applications with Roc. + +## Documentation +- [Actions](/packages/roc-package-web-app-react-dev/docs/Actions.md) +- [Commands](/packages/roc-package-web-app-react-dev/docs/Commands.md) +- [Hooks](/packages/roc-package-web-app-react-dev/docs/Hooks.md) +- [Settings](/packages/roc-package-web-app-react-dev/docs/Settings.md) + +## Runtime +Used with [roc-package-web-app-react](/packages/roc-package-web-app-react). diff --git a/packages/roc-package-web-app-react-dev/bin/index.js b/packages/roc-package-web-app-react-dev/bin/index.js new file mode 100755 index 0000000..a73fd92 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/bin/index.js @@ -0,0 +1,7 @@ +#! /usr/bin/env node + +const pkg = require('../package.json'); + +const initCli = require('roc').initCli; + +initCli(pkg.version, pkg.name); diff --git a/packages/roc-package-web-app-react-dev/package.json b/packages/roc-package-web-app-react-dev/package.json new file mode 100644 index 0000000..ae63ff9 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/package.json @@ -0,0 +1,43 @@ +{ + "name": "roc-package-web-app-react-dev", + "description": "Package for building React applications with Roc (Development)", + "author": "VG", + "license": "MIT", + "version": "1.0.0", + "main": "lib/index.js", + "bin": "bin/index.js", + "scripts": { + "lint": "eslint .", + "test": "npm run lint" + }, + "files": [ + "lib", + "bin" + ], + "keywords": [ + "roc", + "roc-package", + "roc-dev", + "react-router", + "react", + "redux" + ], + "repository": { + "type": "git", + "url": "https://github.com/rocjs/roc-package-web-app-react" + }, + "dependencies": { + "roc": "^1.0.0-rc", + "roc-package-web-app-dev": "^1.0.0-alpha", + "roc-package-web-app-react": "^1.0.0-alpha", + "roc-plugin-react-dev": "^1.0.0-alpha", + + "react-a11y": "~0.2.6", + "yellowbox-react": "~0.9.1" + }, + "devDependencies": { + "babel-eslint": "~5.0.0", + "eslint": "~1.10.3", + "eslint-config-vgno": "~5.0.0" + } +} diff --git a/packages/roc-package-web-app-react-dev/roc.config.js b/packages/roc-package-web-app-react-dev/roc.config.js new file mode 100644 index 0000000..2d0a4b0 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/roc.config.js @@ -0,0 +1,6 @@ +const path = require('path'); + +// Makes it possible for use to generate documentation for this package. +module.exports = { + packages: [path.join(__dirname, 'lib', 'index.js')] +}; diff --git a/packages/roc-package-web-app-react-dev/src/builder/index.js b/packages/roc-package-web-app-react-dev/src/builder/index.js new file mode 100644 index 0000000..4bdb537 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/src/builder/index.js @@ -0,0 +1,112 @@ +import path from 'path'; +import { + getAbsolutePath, + fileExists +} from 'roc'; +import { resolvePath } from 'roc-package-web-app-react'; +/** + * Creates a builder. + * + * @param {!string} target - a target: should be either "client" or "server" + * @param {rocBuilder} rocBuilder - A rocBuilder to base everything on. + * @param {!string} [resolver=roc-web-react/lib/helpers/get-resolve-path] - Path to the resolver for the server side + * {@link getResolvePath} + * @returns {rocBuilder} + */ +export default () => ({ settings: { build: buildSettings }, previousValue: rocBuilder }) => (target) => () => { + let { + buildConfig, + builder, + info + } = rocBuilder; + + const DEV = buildSettings.mode === 'dev'; + const NODE = (target === 'node'); + const WEB = (target === 'web'); + + if (NODE) { + buildConfig.externals = [].concat([ + { + 'roc-package-web-app-react/src/helpers/read-stats': true, + 'roc-package-web-app-react/src/helpers/my-path': true + } + ], buildConfig.externals); + } + + if (WEB) { + buildConfig.plugins.push( + new builder.IgnorePlugin(/^roc$/) + ); + } + + buildConfig.resolveLoader.root.push(path.join(__dirname, '../../node_modules')); + + if (DEV) { + buildConfig.resolve.fallback.push( + path.join(__dirname, '../../node_modules') + ); + } + + buildConfig.resolve.fallback.push(resolvePath); + + if (buildSettings.routes) { + const routes = getAbsolutePath(buildSettings.routes); + + buildConfig.plugins.push( + new builder.DefinePlugin({ + REACT_ROUTER_ROUTES: JSON.stringify(routes) + }) + ); + } + + const hasReducers = !!(buildSettings.reducers && fileExists(buildSettings.reducers)); + if (hasReducers) { + const reducers = getAbsolutePath(buildSettings.reducers); + + buildConfig.plugins.push( + new builder.DefinePlugin({ + REDUX_REDUCERS: JSON.stringify(reducers) + }) + ); + } + + const hasMiddlewares = !!(buildSettings.reduxMiddlewares && fileExists(buildSettings.reduxMiddlewares)); + if (hasMiddlewares) { + const middlewares = getAbsolutePath(buildSettings.reduxMiddlewares); + + buildConfig.plugins.push( + new builder.DefinePlugin({ + REDUX_MIDDLEWARES: JSON.stringify(middlewares) + }) + ); + } + + const hasClientLoading = !!(buildSettings.clientLoading && fileExists(buildSettings.clientLoading)); + if (hasClientLoading) { + const clientLoading = getAbsolutePath(buildSettings.clientLoading); + + buildConfig.plugins.push( + new builder.DefinePlugin({ + ROC_CLIENT_LOADING: JSON.stringify(clientLoading) + }) + ); + } + + buildConfig.plugins.push( + new builder.DefinePlugin({ + USE_DEFAULT_REDUX_REDUCERS: buildSettings.useDefaultReducers, + USE_DEFAULT_REDUX_MIDDLEWARES: buildSettings.useDefaultReduxMiddlewares, + USE_DEFAULT_REACT_ROUTER_ROUTES: buildSettings.useDefaultRoutes, + + HAS_REDUX_REDUCERS: hasReducers, + HAS_REDUX_MIDDLEWARES: hasMiddlewares, + HAS_CLIENT_LOADING: hasClientLoading + }) + ); + + return { + buildConfig, + builder, + info + }; +}; diff --git a/packages/roc-package-web-app-react-dev/src/config/roc.config.js b/packages/roc-package-web-app-react-dev/src/config/roc.config.js new file mode 100644 index 0000000..ba21059 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/src/config/roc.config.js @@ -0,0 +1,45 @@ +export default { + settings: { + build: { + input: { web: '', node: ''}, + + reducers: 'reducers.js', + useDefaultReducers: true, + + routes: 'routes.js', + useDefaultRoutes: true, + + reduxMiddlewares: 'redux-middlewares.js', + useDefaultReduxMiddlewares: true, + + clientLoading: '', + // Consider using the config function to merge this with the previous + resources: ['roc-package-web-app-react/styles/base.css'] + }, + dev: { + // A11Y not play nice with Redux Devtools + a11y: false, + reduxDevtools: { + enabled: true, + position: 'right', + size: 0.3, + visibilityKey: 'H', + positionKey: 'Q', + defaultVisible: false, + theme: 'ocean' + }, + + reduxLogger: { + level: 'info', + collapsed: true, + duration: true, + timestamp: true + }, + + yellowbox: { + enabled: true, + ignore: ['[HMR]', 'Warning: React attempted to reuse markup in a container'] + } + } + } +}; diff --git a/packages/roc-package-web-app-react-dev/src/config/roc.config.meta.js b/packages/roc-package-web-app-react-dev/src/config/roc.config.meta.js new file mode 100644 index 0000000..75b05d8 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/src/config/roc.config.meta.js @@ -0,0 +1,87 @@ +import { isString, isBoolean, isPath, isArray } from 'roc/validators'; + +export default { + settings: { + descriptions: { + build: { + routes: 'The routes to use if no entry file is given, will use default entry files internally.', + useDefaultRoutes: 'If Roc should use an internal wrapper around the routes, please look at the ' + + 'documentation for more details.', + + reducers: 'The reducers to use if no entry file is given, will use default entry files internally.', + useDefaultReducers: 'If Roc should use internally defined reducers, please look at the documentation ' + + ' for what reducers that are included.', + + reduxMiddlewares: 'The middlewares to use if no entry file is given, will use default entry files ' + + 'internally.', + useDefaultReduxMiddlewares: 'If Roc should use internally defined middlewares, please look at the ' + + ' documentation for what middlewares that are included.', + + clientLoading: 'The React component to use on the first client load while fetching data, will only ' + + 'be used if clientBlocking is set to true.' + }, + dev: { + a11y: 'If A11Y validation should be active. Currently it´s suggested to not enable reduxDevtools ' + + 'with this.', + reduxDevtools: { + enabled: 'If Redux Devtools should be enabled.', + position: 'Starting position of the Devtools, can be left, right, top or bottom.', + size: 'Default size of the Devtools, should be a number between 0 and 1.', + visibilityKey: 'The key that should toogle the Redux Devtools, will be combine with CTRL.', + positionKey: 'The key that should change position of the Redux Devtools, will be combine with ' + + 'CTRL.', + defaultVisible: 'If the Redux Devtools should be shown by default.', + theme: 'The theme to use for the Redux Devtools, see ' + + 'https://github.com/gaearon/redux-devtools-themes.' + }, + reduxLogger: { + level: 'The logging level for Redux Logger, can be either warn, error or info.', + collapsed: 'If the logged actions by Redux Logger should be collapsed by default.', + duration: 'If Redux Logger should print the duration of each action.', + timestamp: 'If Redux Logger should print the timestamp with each action.' + }, + yellowbox: { + enabled: 'If YellowBox should be enabled.', + ignore: 'Array of prefix strings that should be ignored by YellowBox.' + } + } + }, + + validations: { + build: { + routes: isPath, + useDefaultRoutes: isBoolean, + + reducers: isPath, + useDefaultReducers: isBoolean, + + reduxMiddlewares: isPath, + useDefaultReduxMiddlewares: isBoolean, + + clientLoading: isPath + }, + dev: { + a11y: isBoolean, + reduxDevtools: { + enabled: isBoolean, + position: /^left|right|top|bottom$/, + size: (input) => input >= 0 && input <= 1, + visibilityKey: isString, + positionKey: isString, + defaultVisible: isBoolean, + theme: isString + }, + reduxLogger: { + level: /^warn|error|info$/, + collapsed: isBoolean, + duration: isBoolean, + timestamp: isBoolean + }, + yellowbox: { + enabled: isBoolean, + ignore: isArray(isString) + } + } + } + } +}; diff --git a/packages/roc-package-web-app-react-dev/src/index.js b/packages/roc-package-web-app-react-dev/src/index.js new file mode 100644 index 0000000..5ed0e86 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/src/index.js @@ -0,0 +1 @@ +export roc from './roc'; diff --git a/packages/roc-package-web-app-react-dev/src/roc/index.js b/packages/roc-package-web-app-react-dev/src/roc/index.js new file mode 100644 index 0000000..1cfad34 --- /dev/null +++ b/packages/roc-package-web-app-react-dev/src/roc/index.js @@ -0,0 +1,55 @@ +import config from '../config/roc.config.js'; +import meta from '../config/roc.config.meta.js'; +import builder from '../builder'; + +import { name } from './util'; + +export default { + name, + config, + meta, + actions: { + webpack: { + hook: 'build-webpack', + action: builder + }, + settings: { + extension: 'roc', + hook: 'update-settings', + action: () => ({ settings }) => () => () => { + const newSettings = { build: { input: {} } }; + + if (!settings.build.input.web) { + newSettings.build.input.web = require.resolve('roc-package-web-app-react/default/client'); + } + + if (!settings.build.input.node) { + newSettings.build.input.node = require.resolve('roc-package-web-app-react/default/server'); + } + + if (settings.build.resources.length > 0) { + const resources = settings.build.resources.map((resource) => { + const matches = /^roc-package-web-app-react\/(.*)/.exec(resource); + if (matches && matches[1]) { + return require.resolve(`roc-package-web-app-react/${matches[1]}`); + } + + return resource; + }); + + newSettings.build.resources = resources; + } + + // If a change has been done we will run the hook + return newSettings; + } + } + }, + packages: [ + require.resolve('roc-package-web-app-dev'), + require.resolve('roc-package-web-app-react') + ], + plugins: [ + require.resolve('roc-plugin-react-dev') + ] +}; diff --git a/packages/roc-package-web-app-react-dev/src/roc/util.js b/packages/roc-package-web-app-react-dev/src/roc/util.js new file mode 100644 index 0000000..90ab38b --- /dev/null +++ b/packages/roc-package-web-app-react-dev/src/roc/util.js @@ -0,0 +1,17 @@ +import { runHook } from 'roc'; + +/** + * The name of the package, for easy consumption. + */ +export const name = require('../../package.json').name; + +/** + * Helper function for invoking/running a hook, pre-configured for the current package. + * + * @param {...Object} args - The arguments to pass along to the action. + * + * @returns {Object|function} - Either a object, the final value from the actions or a function if callback is used. + */ +export function invokeHook(...args) { + return runHook(name)(...args); +} diff --git a/packages/roc-package-web-app-react/.eslintignore b/packages/roc-package-web-app-react/.eslintignore new file mode 100644 index 0000000..bc73336 --- /dev/null +++ b/packages/roc-package-web-app-react/.eslintignore @@ -0,0 +1,3 @@ +lib +esdocs +docs diff --git a/packages/roc-package-web-app-react/.eslintrc b/packages/roc-package-web-app-react/.eslintrc new file mode 100644 index 0000000..811e6a8 --- /dev/null +++ b/packages/roc-package-web-app-react/.eslintrc @@ -0,0 +1,13 @@ +{ + "extends": "vgno", + + "parser": "babel-eslint", + + "env": { + "es6": true + }, + + "ecmaFeatures": { + "modules": true + } +} diff --git a/packages/roc-package-web-app-react/README.md b/packages/roc-package-web-app-react/README.md new file mode 100644 index 0000000..412b294 --- /dev/null +++ b/packages/roc-package-web-app-react/README.md @@ -0,0 +1,11 @@ +# roc-package-web-app-react +Package for building React applications with Roc. + +## Documentation +- [Actions](/packages/roc-package-web-app-react/docs/Actions.md) +- [Commands](/packages/roc-package-web-app-react/docs/Commands.md) +- [Hooks](/packages/roc-package-web-app-react/docs/Hooks.md) +- [Settings](/packages/roc-package-web-app-react/docs/Settings.md) + +## Development +Used with [roc-package-web-app-react-dev](/packages/roc-package-web-app-react-dev). diff --git a/packages/roc-package-web-app-react/app/client/create-client.js b/packages/roc-package-web-app-react/app/client/create-client.js new file mode 100755 index 0000000..9d13a86 --- /dev/null +++ b/packages/roc-package-web-app-react/app/client/create-client.js @@ -0,0 +1,141 @@ +/* global __DIST__ __TEST__ HAS_CLIENT_LOADING ROC_CLIENT_LOADING ROC_PATH */ + +import React from 'react'; +import ReactDom from 'react-dom'; + +import createHistory from 'history/lib/createBrowserHistory'; +import useBasename from 'history/lib/useBasename'; +import { Router } from 'react-router'; + +import { Provider } from 'react-redux'; +import { syncReduxAndRouter } from 'redux-simple-router'; +import debug from 'debug'; + +import { rocConfig } from '../shared/universal-config'; + +const clientDebug = debug('roc:client'); + +const basename = rocConfig.runtime.path === '/' ? null : rocConfig.runtime.path; + +/** + * Client entry point for React applications. + * + * @example + * import { createClient } from 'roc-web-react/app/client'; + * + * const server = createClient({ + * createRoutes: routes, + * createStore: store, + * mountNode: 'application' + * }); + * + * @param {rocClientOptions} options - Options for the client + */ +export default function createClient({ createRoutes, createStore, mountNode }) { + if (!createRoutes) { + throw new Error(`createRoutes needs to be defined`); + } + + if (!mountNode) { + throw new Error(`mountNode needs to be defined`); + } + + if (rocConfig) { + debug.enable(rocConfig.runtime.debug.client); + } + + if (!__DIST__ && rocConfig.dev.a11y) { + if (rocConfig.runtime.ssr) { + clientDebug('You will see a "Warning: React attempted to reuse markup in a container but the checksum was' + + ' invalid." message. That\'s because a11y is enabled.'); + } + + require('react-a11y')(React); + } + + const render = () => { + const node = document.getElementById(mountNode); + + let component; + const history = useBasename(createHistory)({ basename }); + + if (createStore) { + const store = createStore(window.FLUX_STATE); + + const ReduxContext = require('./redux-context').default; + + let initalClientLoading = null; + if (HAS_CLIENT_LOADING) { + initalClientLoading = require(ROC_CLIENT_LOADING).default; + } + + component = ( + + ); + + syncReduxAndRouter(history, store); + + if (!__DIST__) { + if (rocConfig.dev.reduxDevtools.enabled) { + const DevTools = require('./dev-tools').default; + + if (rocConfig.runtime.ssr) { + clientDebug('You will see a "Warning: React attempted to reuse markup in a container but the ' + + 'checksum was invalid." message. That\'s because the redux-devtools are enabled.'); + } + + component = ( +
+ { component } + +
+ ); + } + + if (rocConfig.dev.yellowbox.enabled) { + const YellowBox = require('yellowbox-react').default; + + /* eslint-disable no-console */ + console.ignoredYellowBox = rocConfig.dev.yellowbox.ignore; + /* eslint-enable */ + + if (rocConfig.runtime.ssr) { + clientDebug('You will see a "Warning: React attempted to reuse markup in a container but the ' + + 'checksum was invalid." message. That\'s because the YellowBox is enabled.'); + } + + component = ( +
+ { component } + +
+ ); + } + } + + component = ( + + { component } + + ); + } else { + component = ( + + ); + } + + ReactDom.render(component, node); + }; + + render(); +} diff --git a/packages/roc-package-web-app-react/app/client/dev-tools.js b/packages/roc-package-web-app-react/app/client/dev-tools.js new file mode 100755 index 0000000..409f900 --- /dev/null +++ b/packages/roc-package-web-app-react/app/client/dev-tools.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { createDevTools } from 'redux-devtools'; +import LogMonitor from 'redux-devtools-log-monitor'; +import DockMonitor from 'redux-devtools-dock-monitor'; + +import { rocConfig } from '../shared/universal-config'; + +export default createDevTools( + + + +); diff --git a/packages/roc-package-web-app-react/app/client/index.js b/packages/roc-package-web-app-react/app/client/index.js new file mode 100755 index 0000000..f9ecfbc --- /dev/null +++ b/packages/roc-package-web-app-react/app/client/index.js @@ -0,0 +1 @@ +export createClient from './create-client'; diff --git a/packages/roc-package-web-app-react/app/client/redux-context/index.js b/packages/roc-package-web-app-react/app/client/redux-context/index.js new file mode 100755 index 0000000..4321794 --- /dev/null +++ b/packages/roc-package-web-app-react/app/client/redux-context/index.js @@ -0,0 +1,138 @@ +import React from 'react'; +import RoutingContext from 'react-router/lib/RoutingContext'; +import { getPrefetchedData, getDeferredData } from 'react-fetcher'; + +import ReduxContextContainer from './redux-context-container'; + +export default class ReduxContext extends React.Component { + + static childContextTypes = { + reduxContext: React.PropTypes.object + }; + + static propTypes = { + components: React.PropTypes.array.isRequired, + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired, + store: React.PropTypes.object.isRequired, + blocking: React.PropTypes.bool, + initalClientLoading: React.PropTypes.func + }; + + static defaultProps = { + blocking: false + }; + + constructor(props, context) { + super(props, context); + this.state = { + loading: false, + prevProps: null, + inital: true && this.props.blocking + }; + } + + getChildContext() { + const { loading } = this.state; + return { + reduxContext: { + loading + } + }; + } + + componentWillReceiveProps(nextProps) { + const routeChanged = nextProps.location !== this.props.location; + if (!routeChanged) { + return; + } + + const newComponents = this.filterAndFlattenComponents(nextProps.components, 'fetchers'); + if (newComponents.length > 0) { + this.loadData(newComponents, nextProps.params, nextProps.location); + } else { + this.setState({ + loading: false, + prevProps: null + }); + } + + const newComponentsDefered = this.filterAndFlattenComponents(nextProps.components, 'deferredFetchers'); + if (newComponentsDefered.length > 0) { + this.loadDataDefered(newComponentsDefered, nextProps.params, nextProps.location); + } + } + + componentWillUnmount() { + this.unmounted = true; + } + + createElement(Component, props) { + return ( + + ); + } + + filterAndFlattenComponents(components, staticMethod) { + return components.filter((component) => !!component[staticMethod]); + } + + loadDataDefered(components, params, location) { + // Get deferred data, will not block route transitions + getDeferredData(components, { + location, + params, + dispatch: this.props.store.dispatch, + getState: this.props.store.getState + }).catch((err) => { + if (process.env.NODE_ENV !== 'production') { + console.error('There was an error when fetching data: ', err); + } + }); + } + + loadData(components, params, location) { + const completeRouteTransition = () => { + const sameLocation = this.props.location === location; + + if (sameLocation && !this.unmounted) { + this.setState({ + loading: false, + prevProps: null, + inital: false + }); + } + }; + + if (this.props.blocking) { + this.setState({ + loading: true, + prevProps: this.props + }); + } + + getPrefetchedData(components, { + location, + params, + dispatch: this.props.store.dispatch, + getState: this.props.store.getState + }).then(() => { + completeRouteTransition(); + }).catch((err) => { + if (process.env.NODE_ENV !== 'production') { + console.error('There was an error when fetching data: ', err); + } + + completeRouteTransition(); + }); + } + + render() { + if (this.props.initalClientLoading && this.state.inital) { + return ; + } + + const props = this.state.loading ? this.state.prevProps : this.props; + return ; + } +} diff --git a/packages/roc-package-web-app-react/app/client/redux-context/redux-context-container.js b/packages/roc-package-web-app-react/app/client/redux-context/redux-context-container.js new file mode 100755 index 0000000..4a5f44c --- /dev/null +++ b/packages/roc-package-web-app-react/app/client/redux-context/redux-context-container.js @@ -0,0 +1,25 @@ +import React from 'react'; + +export default class ReduxContextContainer extends React.Component { + + static propTypes = { + Component: React.PropTypes.func.isRequired, + routerProps: React.PropTypes.object.isRequired + }; + + static contextTypes = { + reduxContext: React.PropTypes.object.isRequired + }; + + render() { + const { Component, routerProps } = this.props; + const { loading } = this.context.reduxContext; + + return ( + + ); + } +} diff --git a/packages/roc-package-web-app-react/app/server/index.js b/packages/roc-package-web-app-react/app/server/index.js new file mode 100755 index 0000000..5e109b2 --- /dev/null +++ b/packages/roc-package-web-app-react/app/server/index.js @@ -0,0 +1,2 @@ +export useReact from './use-react'; +export { createServer } from 'roc-package-web-app/app'; diff --git a/packages/roc-package-web-app-react/app/server/render.js b/packages/roc-package-web-app-react/app/server/render.js new file mode 100755 index 0000000..a5c3111 --- /dev/null +++ b/packages/roc-package-web-app-react/app/server/render.js @@ -0,0 +1,137 @@ +/* global __DIST__ __DEV__ ROC_PATH */ + +import debug from 'debug'; +import nunjucks from 'nunjucks'; +import serialize from 'serialize-javascript'; +import PrettyError from 'pretty-error'; +import React from 'react'; +import { renderToString, renderToStaticMarkup } from 'react-dom/server'; +import { match, RoutingContext } from 'react-router'; +import { Provider } from 'react-redux'; +import { updatePath } from 'redux-simple-router'; +import Helmet from 'react-helmet'; +import { getPrefetchedData } from 'react-fetcher'; +import { getAbsolutePath } from 'roc'; +import ServerStatus from 'react-server-status'; +import myPath from 'roc-package-web-app-react/lib/helpers/my-path'; + +import { rocConfig, appConfig } from '../shared/universal-config'; +import Header from '../shared/header'; + +const pretty = new PrettyError(); +const log = debug('roc:react-render'); + +export function initRenderPage({ script, css }) { + const templatePath = rocConfig.runtime.template.path || `${myPath}/views`; + nunjucks.configure(getAbsolutePath(templatePath), { + watch: __DEV__ + }); + + const bundleName = script[0]; + const styleName = css[0]; + + return ( + head, + content = '', + fluxState = {} + ) => { + const { dev, build, ...rest } = rocConfig; + + const rocConfigClient = __DIST__ ? rest : {...rest, dev}; + + // If we have no head we will generate it + if (!head) { + // Render to trigger React Helmet + renderToStaticMarkup(
); + head = Helmet.rewind(); + } + + return nunjucks.render(rocConfig.runtime.template.name, { + head, + content, + fluxState: serialize(fluxState), + bundleName, + styleName, + dist: __DIST__, + serializedRocConfig: serialize(rocConfigClient), + serializedAppConfig: serialize(appConfig) + }); + }; +} + +export function reactRender(url, createRoutes, store, renderPage, staticRender = false) { + const basename = rocConfig.runtime.path === '/' ? null : rocConfig.runtime.path; + + return new Promise((resolve) => { + match({routes: createRoutes(store), location: url, basename }, + (error, redirect, renderProps) => { + if (redirect) { + log(`Redirect request to ${redirect.pathname + redirect.search}`); + return resolve({ + redirect: redirect.pathname + redirect.search + }); + } else if (error) { + log('Router error', pretty.render(error)); + return resolve({ + status: 500, + body: renderPage() + }); + } else if (!renderProps) { + log('No renderProps, most likely the path does not exist'); + return resolve({ + status: 500, + body: renderPage() + }); + } + + const components = renderProps.routes.map(route => route.component); + + let locals = { + location: renderProps.location, + params: renderProps.params + }; + + if (store) { + locals = { + ...locals, + dispatch: store.dispatch, + getState: store.getState + }; + } + + getPrefetchedData(components, locals) + .then(() => { + let component = ; + + if (store) { + store.dispatch(updatePath(url)); + + component = ( + + { component } + + ); + } + + const page = staticRender ? renderToStaticMarkup(component) : renderToString(component); + const head = Helmet.rewind(); + + const state = store ? store.getState() : {}; + + return resolve({ + body: renderPage(head, page, state), + status: ServerStatus.rewind() || 200 + }); + }) + .catch((err) => { + if (err) { + log('Fetching error', pretty.render(err)); + return resolve({ + status: 500, + body: renderPage() + }); + } + }); + }); + }); +} diff --git a/packages/roc-package-web-app-react/app/server/router.js b/packages/roc-package-web-app-react/app/server/router.js new file mode 100755 index 0000000..b6b748e --- /dev/null +++ b/packages/roc-package-web-app-react/app/server/router.js @@ -0,0 +1,64 @@ +import debug from 'debug'; +import PrettyError from 'pretty-error'; + +import { rocConfig } from '../shared/universal-config'; + +import { initRenderPage, reactRender } from './render'; + +const pretty = new PrettyError(); +const log = debug('roc:server'); + +export default function routes({ createRoutes, createStore, stats, dist }) { + if (!createRoutes) { + throw new Error('createRoutes needs to be defined'); + } + + if (!stats) { + throw new Error('stats needs to be defined'); + } + + const renderPage = initRenderPage(stats, dist); + + return function* (next) { + try { + // If server side rendering is disabled we do everything on the client + if (!rocConfig.runtime.ssr) { + yield next; + + // If response already is managed we will not do anything + if (this.body || this.status !== 404) { + return; + } + + this.status = 200; + this.body = renderPage(); + } else { + const store = createStore ? createStore() : null; + this.state.reduxStore = store; + yield next; + + // If response already is managed we will not do anything + if (this.body || this.status !== 404) { + return; + } + + const { + body, + redirect, + status = 200 + } = yield reactRender(this.url, createRoutes, store, renderPage); + + if (redirect) { + this.redirect(redirect); + } else { + this.status = status; + this.body = body; + } + } + } catch (error) { + log('Render error', pretty.render(error)); + this.status = 500; + this.body = renderPage(); + } + }; +} diff --git a/packages/roc-package-web-app-react/app/server/use-react.js b/packages/roc-package-web-app-react/app/server/use-react.js new file mode 100755 index 0000000..6425b00 --- /dev/null +++ b/packages/roc-package-web-app-react/app/server/use-react.js @@ -0,0 +1,36 @@ +import readStats from 'roc-package-web-app-react/lib/helpers/read-stats'; +import routes from './router'; + +/** + * Enhances a server instance with React support. + * + * Extends the options object from _roc-web_. See {@link rocServerOptions} for what the new options are. + * + * @example + * import { createServer } from 'roc-web/app'; + * import { useReact } from 'roc-web-react/app/server'; + * + * const server = useReact(createServer)({ + * serve: 'files', + * createRoutes: routes, + * createStore: store, + * stats: './stats.json' + * }); + + * server.start(); + * + * @param {function} createServer - A createServer function to wrap and add extra functionality to + * @returns {function} Returns a new createServer that can be used to create server instances that can manage React + * applications + */ +export default function useReact(createServer) { + return function(options = {}, beforeUserMiddlewares = []) { + const { stats, createRoutes, createStore, ...serverOptions } = options; + + return createServer(serverOptions, beforeUserMiddlewares.concat(routes({ + createRoutes, + createStore, + stats: readStats(stats) + }))); + }; +} diff --git a/packages/roc-package-web-app-react/app/shared/application.js b/packages/roc-package-web-app-react/app/shared/application.js new file mode 100755 index 0000000..8dc77a9 --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/application.js @@ -0,0 +1,19 @@ +import React from 'react'; + +import Header from './header'; + +export default class Application extends React.Component { + + static propTypes = { + children: React.PropTypes.node + }; + + render() { + return ( +
+
+ { this.props.children } +
+ ); + } +} diff --git a/packages/roc-package-web-app-react/app/shared/create-routes.js b/packages/roc-package-web-app-react/app/shared/create-routes.js new file mode 100755 index 0000000..d780a04 --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/create-routes.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Route } from 'react-router'; + +import Application from './application'; + +/** + * Route creator + * + * @param {!function} routes - A function that takes a reference to potential store and returns a React Router route + * @returns {function} A function that takes a reference to a potential store, runs the `routes` function and wrapps the + * result in a _Application component_ wrapper. See the README.md for more information on what it does. + */ +export default function createRoutes(routes) { + return store => { + const appRoutes = routes(store); + + return ( + + { appRoutes } + + ); + }; +} diff --git a/packages/roc-package-web-app-react/app/shared/flux/create-store.js b/packages/roc-package-web-app-react/app/shared/flux/create-store.js new file mode 100755 index 0000000..a7fac91 --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/flux/create-store.js @@ -0,0 +1,67 @@ +/* globals __DEV__ __WEB__ */ + +import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; +import { routeReducer } from 'redux-simple-router'; + +import { rocConfig } from '../universal-config'; + +/** + * Redux store creator + * + * @param {!object} reducers - Reducers that should be added to the store + * @param {...function} middlewares - Redux middlewares that should be added to the store + * @returns {function} A function that has the following interface: + * `(callback) => (reduxReactRouter, getRoutes, createHistory, initialState)`. + * The callback will be called when the application is in _DEV_ mode on the client as a way to add hot module update of + * the reducers. The callback itself will take a function as the parameter that in turn takes the reducers to update. + */ +export default function createReduxStore(reducers, ...middlewares) { + return (callback) => + (initialState) => { + let finalCreateStore; + + if (__DEV__ && __WEB__) { + const { persistState } = require('redux-devtools'); + const { instrument } = require('../../client/dev-tools').default; + const createLogger = require('redux-logger'); + const logger = createLogger({ + level: rocConfig.dev.reduxLogger.level, + collapsed: rocConfig.dev.reduxLogger.collapsed, + duration: rocConfig.dev.reduxLogger.duration, + timestamp: rocConfig.dev.reduxLogger.timestamp + }); + + const debugMiddlewares = [logger]; + + finalCreateStore = compose( + applyMiddleware(...middlewares, ...debugMiddlewares), + instrument(), + persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) + )(createStore); + } else { + finalCreateStore = compose( + applyMiddleware(...middlewares) + )(createStore); + } + + const reducer = combineReducers({ + routing: routeReducer, + ...reducers + }); + + const store = finalCreateStore(reducer, initialState); + + if (__DEV__ && __WEB__ && module.hot) { + // Enable Webpack hot module replacement for reducers + callback((newReducers) => { + const nextRootReducer = combineReducers({ + routing: routeReducer, + ...newReducers + }); + store.replaceReducer(nextRootReducer); + }); + } + + return store; + }; +} diff --git a/packages/roc-package-web-app-react/app/shared/flux/middlewares/index.js b/packages/roc-package-web-app-react/app/shared/flux/middlewares/index.js new file mode 100755 index 0000000..8c023ec --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/flux/middlewares/index.js @@ -0,0 +1,4 @@ +import { apiMiddleware } from 'redux-api-middleware'; +import thunk from 'redux-thunk'; + +export default [thunk, apiMiddleware]; diff --git a/packages/roc-package-web-app-react/app/shared/flux/reducers/errors.js b/packages/roc-package-web-app-react/app/shared/flux/reducers/errors.js new file mode 100755 index 0000000..5ba411d --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/flux/reducers/errors.js @@ -0,0 +1,14 @@ +export default function errors(state = [], action) { + const { type, error, payload } = action; + + if (type === 'RESET_ERROR_MESSAGES') { + return []; + } else if (error) { + return [ + ...state, + payload + ]; + } + + return state; +} diff --git a/packages/roc-package-web-app-react/app/shared/flux/reducers/index.js b/packages/roc-package-web-app-react/app/shared/flux/reducers/index.js new file mode 100755 index 0000000..58bfa94 --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/flux/reducers/index.js @@ -0,0 +1 @@ +export errors from './errors'; diff --git a/packages/roc-package-web-app-react/app/shared/header.js b/packages/roc-package-web-app-react/app/shared/header.js new file mode 100755 index 0000000..f7b38b2 --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/header.js @@ -0,0 +1,24 @@ +import React from 'react'; +import Helmet from 'react-helmet'; + +import { rocConfig } from './universal-config'; + +export default class Header extends React.Component { + render() { + const path = rocConfig.runtime.path !== '/' ? rocConfig.runtime.path + '/' : rocConfig.runtime.path; + const base = rocConfig.runtime.base.href ? { + ...rocConfig.runtime.base, + href: rocConfig.runtime.base.href.replace(new RegExp(rocConfig.runtime.path), path) + } : {}; + + return ( + + ); + } +} diff --git a/packages/roc-package-web-app-react/app/shared/index.js b/packages/roc-package-web-app-react/app/shared/index.js new file mode 100755 index 0000000..afaf748 --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/index.js @@ -0,0 +1,6 @@ +export { rocConfig } from './universal-config'; +export { appConfig } from './universal-config'; +export createRoutes from './create-routes'; +export createStore from './flux/create-store'; +export defaultReducers from './flux/reducers'; +export defaultMiddlewares from './flux/middlewares'; diff --git a/packages/roc-package-web-app-react/app/shared/universal-config.js b/packages/roc-package-web-app-react/app/shared/universal-config.js new file mode 100755 index 0000000..a89f86c --- /dev/null +++ b/packages/roc-package-web-app-react/app/shared/universal-config.js @@ -0,0 +1,22 @@ + /** + * Universal Configuration Manager + * + * Manages both __application__ configuration and __Roc__ configuration. + * On the server the configurations will be fetched directly and on the client it's expected that the configuration + * is available on `window` as `ROC_CONFIG` and `APP_CONFIG`. + * + * appConfig will only contain what has been selected by `runtime.configWhitelistProperty`. That means if you want + * to read the full configuration on the server you will need to read it directly from node-config. + */ + +export const rocConfig = (function() { + return typeof window !== 'undefined' ? window.ROC_CONFIG : require('roc').getSettings(); +})(); + +const whiteListed = () => rocConfig.runtime.configWhitelistProperty ? + require('config')[rocConfig.runtime.configWhitelistProperty] : + undefined; + +export const appConfig = (function() { + return typeof window !== 'undefined' ? window.APP_CONFIG : whiteListed(); +})(); diff --git a/packages/roc-package-web-app-react/bin/index.js b/packages/roc-package-web-app-react/bin/index.js new file mode 100755 index 0000000..a73fd92 --- /dev/null +++ b/packages/roc-package-web-app-react/bin/index.js @@ -0,0 +1,7 @@ +#! /usr/bin/env node + +const pkg = require('../package.json'); + +const initCli = require('roc').initCli; + +initCli(pkg.version, pkg.name); diff --git a/packages/roc-package-web-app-react/default/client.js b/packages/roc-package-web-app-react/default/client.js new file mode 100755 index 0000000..5d96b2e --- /dev/null +++ b/packages/roc-package-web-app-react/default/client.js @@ -0,0 +1,10 @@ +import { createClient } from '../app/client'; +import getRoutesAndStore from './get-routes-and-store'; + +const { store, routes } = getRoutesAndStore(true); + +createClient({ + createRoutes: routes, + createStore: store, + mountNode: 'application' +}); diff --git a/packages/roc-package-web-app-react/default/get-routes-and-store.js b/packages/roc-package-web-app-react/default/get-routes-and-store.js new file mode 100755 index 0000000..b847358 --- /dev/null +++ b/packages/roc-package-web-app-react/default/get-routes-and-store.js @@ -0,0 +1,62 @@ +/* global REACT_ROUTER_ROUTES REDUX_REDUCERS HAS_REDUX_REDUCERS HAS_REDUX_MIDDLEWARES REDUX_MIDDLEWARES + USE_DEFAULT_REDUX_REDUCERS USE_DEFAULT_REDUX_MIDDLEWARES USE_DEFAULT_REACT_ROUTER_ROUTES +*/ + +export default function getRoutesAndStore(web = false) { + let store = null; + let routes = null; + + if (HAS_REDUX_REDUCERS) { + const { createStore } = require('../app/shared'); + + let defaultReducers = {}; + if (USE_DEFAULT_REDUX_REDUCERS) { + defaultReducers = require('../app/shared').defaultReducers; + } + + let middlewares = []; + if (USE_DEFAULT_REDUX_MIDDLEWARES) { + middlewares = middlewares.concat(require('../app/shared').defaultMiddlewares); + } + + if (HAS_REDUX_MIDDLEWARES) { + middlewares = middlewares.concat(require(REDUX_MIDDLEWARES).default()); + } + + const reducers = { + ...defaultReducers, + ...require(REDUX_REDUCERS) + }; + + const storeCreator = createStore( + reducers, + ...middlewares + ); + + let replaceReducers = null; + if (web) { + replaceReducers = (replaceReducer) => { + module.hot.accept(require.resolve(REDUX_REDUCERS), () => { + replaceReducer({ + ...defaultReducers, + ...require(REDUX_REDUCERS) + }); + }); + }; + } + + store = storeCreator(replaceReducers); + } + + if (USE_DEFAULT_REACT_ROUTER_ROUTES) { + const { createRoutes } = require('../app/shared'); + routes = createRoutes(require(REACT_ROUTER_ROUTES).default); + } else { + routes = require(REACT_ROUTER_ROUTES).default; + } + + return { + routes, + store + }; +} diff --git a/packages/roc-package-web-app-react/default/server.js b/packages/roc-package-web-app-react/default/server.js new file mode 100755 index 0000000..838d555 --- /dev/null +++ b/packages/roc-package-web-app-react/default/server.js @@ -0,0 +1,10 @@ +import { createServer, useReact } from '../app/server'; + +import getRoutesAndStore from './get-routes-and-store'; + +const { store, routes } = getRoutesAndStore(); + +useReact(createServer)({ + createRoutes: routes, + createStore: store +}).start(); diff --git a/packages/roc-package-web-app-react/package.json b/packages/roc-package-web-app-react/package.json new file mode 100644 index 0000000..f4d378c --- /dev/null +++ b/packages/roc-package-web-app-react/package.json @@ -0,0 +1,63 @@ +{ + "name": "roc-package-web-app-react", + "description": "Package for building React applications with Roc", + "author": "VG", + "license": "MIT", + "version": "1.0.0", + "main": "lib/index.js", + "bin": "bin/index.js", + "scripts": { + "lint": "eslint .", + "test": "npm run lint" + }, + "files": [ + "lib", + "styles", + "views", + "default", + "bin", + "app" + ], + "keywords": [ + "roc", + "roc-package", + "react-router", + "react", + "redux" + ], + "repository": { + "type": "git", + "url": "https://github.com/rocjs/roc-package-web-app-react" + }, + "dependencies": { + "debug": "~2.2.0", + "error": "~7.0.2", + "history": "~1.12.5", + "nunjucks": "~2.1.0", + "pretty-error": "~1.2.0", + "react-a11y": "~0.2.6", + "react-fetcher": "~0.2.0", + "react-helmet": "~2.3.1", + "react-proxy": "1.1.8", + "react-redux": "~4.0.0", + "react-router": "~1.0.0", + "react-server-status": "~1.0.0", + "redux": "~3.0.2", + "redux-api-middleware": "vgno/redux-api-middleware#v1.0.0-beta5", + "redux-devtools": "v3.0.0-beta-3", + "redux-devtools-dock-monitor": "1.0.0-beta-3", + "redux-devtools-log-monitor": "1.0.0-beta-3", + "redux-logger": "~2.1.3", + "redux-simple-router": "~0.0.8", + "redux-thunk": "~1.0.0", + "roc": "^1.0.0-rc", + "roc-package-web-app": "^1.0.0-alpha", + "roc-plugin-react": "^1.0.0-alpha", + "serialize-javascript": "~1.1.1" + }, + "devDependencies": { + "babel-eslint": "~5.0.0", + "eslint": "~1.10.3", + "eslint-config-vgno": "~5.0.0" + } +} diff --git a/packages/roc-package-web-app-react/roc.config.js b/packages/roc-package-web-app-react/roc.config.js new file mode 100644 index 0000000..2d0a4b0 --- /dev/null +++ b/packages/roc-package-web-app-react/roc.config.js @@ -0,0 +1,6 @@ +const path = require('path'); + +// Makes it possible for use to generate documentation for this package. +module.exports = { + packages: [path.join(__dirname, 'lib', 'index.js')] +}; diff --git a/packages/roc-package-web-app-react/src/config/roc.config.js b/packages/roc-package-web-app-react/src/config/roc.config.js new file mode 100644 index 0000000..198b50d --- /dev/null +++ b/packages/roc-package-web-app-react/src/config/roc.config.js @@ -0,0 +1,32 @@ +export default { + settings: { + runtime: { + stats: 'build/client/webpack-stats.json', + applicationName: '', + meta: [{ + name: 'viewport', + content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' + }], + link: [{ + rel: 'icon', + href: 'favicon.png' + }], + // ROC_PATH will be replaced with what is defined in build.path + base: { + href: 'ROC_PATH', + target: '' + }, + script: [], + ssr: true, + clientBlocking: false, + template: { + path: '', + name: 'main.html' + }, + debug: { + client: 'roc:*' + }, + configWhitelistProperty: 'DANGEROUSLY_EXPOSE_TO_CLIENT' + } + } +}; diff --git a/packages/roc-package-web-app-react/src/config/roc.config.meta.js b/packages/roc-package-web-app-react/src/config/roc.config.meta.js new file mode 100644 index 0000000..3944bed --- /dev/null +++ b/packages/roc-package-web-app-react/src/config/roc.config.meta.js @@ -0,0 +1,66 @@ +import { isString, isBoolean, isPath, isArray, isObject, required } from 'roc/validators'; + +export default { + settings: { + groups: { + runtime: { + base: 'Base tag to be used in , ' + + 'see https://github.com/nfl/react-helmet.' + } + }, + descriptions: { + runtime: { + stats: 'Path to client stats file from build.', + applicationName: 'Application name to use for .', + meta: 'Meta tags to be used in <head>, should be formatted as objects, ' + + 'see https://github.com/nfl/react-helmet.', + link: 'Link tags to be used in <head>, should be formatted as objects, ' + + 'See https://github.com/nfl/react-helmet.', + base: { + href: 'The document base address from which relative links are made.', + target: 'The browsing context in which the links should open.' + }, + script: 'Script tags to be used in <head>, should be formatted as objects, ' + + 'See https://github.com/nfl/react-helmet.', + ssr: 'If server side rendering should be enabled.', + clientBlocking: 'If "prefetch" should block a route transition on the client.', + template: { + path: 'A directory where the template for the application can be found. Will default to internal ' + + 'path.', + name: 'Name of the template file that will be used. Uses Nunjucks, please see documentation for ' + + 'more info.' + }, + debug: { + client: 'Filter for debug messages that should be shown for the client, see ' + + 'https://npmjs.com/package/debug.' + }, + configWhitelistProperty: 'A single property to expose to the client from node-config. Make sure that ' + + 'this property does NOT contain any secrets that should not be exposed to the world.' + } + }, + + validations: { + runtime: { + stats: isPath, + applicationName: required(isString), + meta: isArray(isObject(isString)), + link: isArray(isObject(isString)), + base: { + href: isString, + target: isString + }, + script: isArray(isObject(isString)), + ssr: isBoolean, + clientBlocking: isBoolean, + template: { + path: isPath, + name: isString + }, + debug: { + client: isString + }, + configWhitelistProperty: isString + } + } + } +}; diff --git a/packages/roc-package-web-app-react/src/helpers/my-path.js b/packages/roc-package-web-app-react/src/helpers/my-path.js new file mode 100755 index 0000000..0f03912 --- /dev/null +++ b/packages/roc-package-web-app-react/src/helpers/my-path.js @@ -0,0 +1,8 @@ +import 'source-map-support/register'; + +import path from 'path'; + +/** + * Exports the path to the root of the project + */ +export default path.join(__dirname, '..', '..'); diff --git a/packages/roc-package-web-app-react/src/helpers/read-stats.js b/packages/roc-package-web-app-react/src/helpers/read-stats.js new file mode 100755 index 0000000..0a3c6d6 --- /dev/null +++ b/packages/roc-package-web-app-react/src/helpers/read-stats.js @@ -0,0 +1,13 @@ +import { getSettings, getAbsolutePath } from 'roc'; + +/** + * Read stats from build + * + * @param {string} stats - Path to a stats file from the client build + * @returns {object} The stats object in the stats file + */ +export default function readStats(stats) { + const settings = getSettings('runtime'); + + return require(getAbsolutePath(stats || settings.stats)); +} diff --git a/packages/roc-package-web-app-react/src/index.js b/packages/roc-package-web-app-react/src/index.js new file mode 100644 index 0000000..4e03014 --- /dev/null +++ b/packages/roc-package-web-app-react/src/index.js @@ -0,0 +1,3 @@ +export roc from './roc'; + +export resolvePath from './resolver'; diff --git a/packages/roc-package-web-app-react/src/resolver/index.js b/packages/roc-package-web-app-react/src/resolver/index.js new file mode 100644 index 0000000..e17f543 --- /dev/null +++ b/packages/roc-package-web-app-react/src/resolver/index.js @@ -0,0 +1,5 @@ +import { join } from 'path'; + +const resolvePath = join(__dirname, '..', '..', 'node_modules'); + +export default resolvePath; diff --git a/packages/roc-package-web-app-react/src/roc/index.js b/packages/roc-package-web-app-react/src/roc/index.js new file mode 100644 index 0000000..5f7cd29 --- /dev/null +++ b/packages/roc-package-web-app-react/src/roc/index.js @@ -0,0 +1,22 @@ +import resolvePath from '../resolver'; +import config from '../config/roc.config'; +import meta from '../config/roc.config.meta'; + +export default { + name: require('../../package.json').name, + config, + meta, + actions: { + react: { + extension: 'roc-plugin-start', + hook: 'get-resolve-paths', + action: () => () => () => () => resolvePath + } + }, + packages: [ + require.resolve('roc-package-web-app') + ], + plugins: [ + require.resolve('roc-plugin-react') + ] +}; diff --git a/packages/roc-package-web-app-react/styles/base.css b/packages/roc-package-web-app-react/styles/base.css new file mode 100755 index 0000000..e64d1d3 --- /dev/null +++ b/packages/roc-package-web-app-react/styles/base.css @@ -0,0 +1,5 @@ +* { + box-sizing: border-box; + margin: 0px; + padding: 0px; +} diff --git a/packages/roc-package-web-app-react/views/main.html b/packages/roc-package-web-app-react/views/main.html new file mode 100755 index 0000000..0d6cc40 --- /dev/null +++ b/packages/roc-package-web-app-react/views/main.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta http-equiv="x-ua-compatible" content="ie=edge"> + + {{ head.title | safe }} + {{ head.meta | safe }} + {{ head.link | safe }} + {{ head.base | safe }} + {{ head.script | safe }} + + {% if dist %} + <link rel="stylesheet" href="{{ styleName }}"> + {% endif %} + </head> + <body> + <div id="application">{{ content | safe }}</div> + + <script>window.ROC_CONFIG = {{ serializedRocConfig | safe }}</script> + <script>window.APP_CONFIG = {{ serializedAppConfig | safe }}</script> + <script>window.FLUX_STATE = {{ fluxState | safe }}</script> + + <script src="{{ bundleName }}"></script> + </body> +</html>