+ );
+ }
+}
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 , should be formatted as objects, ' +
+ 'see https://github.com/nfl/react-helmet.',
+ link: 'Link tags to be used in , 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 , 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 @@
+
+
+
+
+
+
+ {{ head.title | safe }}
+ {{ head.meta | safe }}
+ {{ head.link | safe }}
+ {{ head.base | safe }}
+ {{ head.script | safe }}
+
+ {% if dist %}
+
+ {% endif %}
+
+
+