Skip to content

Commit

Permalink
[major] update redux router engine to react router 4
Browse files Browse the repository at this point in the history
  • Loading branch information
jchip committed Jun 26, 2018
1 parent 072b918 commit de76db2
Show file tree
Hide file tree
Showing 23 changed files with 973 additions and 448 deletions.
279 changes: 164 additions & 115 deletions docs/chapter1/advanced/stand-alone-modules/redux-router-engine.md

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion packages/electrode-redux-router-engine/.babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
presets: ["es2015", "stage-0", "react"]
"presets": [ ["env",
{
"targets": {
"node": "8"
}
}],
"stage-0", "react"]
}
2 changes: 1 addition & 1 deletion packages/electrode-redux-router-engine/LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2016 WalmartLabs
Copyright (c) 2016-present, WalmartLabs

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
20 changes: 7 additions & 13 deletions packages/electrode-redux-router-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,24 @@ $ npm install -save electrode-redux-router-engine

This module is a part of the [Electrode Platform].

See documentation [here](https://electrode.gitbooks.io/electrode/content/chapter1/advanced/stand-alone-modules/redux-router-engine.html) for details on usage of APIs and options.
See documentation [here](https://docs.electrode.io/advanced/stand-alone-modules/redux-router-engine) for details on usage of APIs and options.

Built with :heart: by [Team Electrode](https://github.com/orgs/electrode-io/people) @WalmartLabs.
## License

[electrode platform]: http://www.electrode.io/
Copyright (c) 2016-present, WalmartLabs

[redux async actions]: http://redux.js.org/docs/advanced/AsyncActions.html
Licensed under the [Apache License, Version 2.0].

[apache license, version 2.0]: https://www.apache.org/licenses/LICENSE-2.0
[electrode platform]: http://www.electrode.io/
[redux async actions]: http://redux.js.org/docs/advanced/AsyncActions.html
[redux server rendering]: http://redux.js.org/docs/recipes/ServerRendering.html

[react-router]: https://github.com/reactjs/react-router

[npm-image]: https://badge.fury.io/js/electrode-redux-router-engine.svg

[npm-url]: https://npmjs.org/package/electrode-redux-router-engine

[daviddm-image]: https://david-dm.org/electrode-io/electrode/status.svg?path=packages/electrode-redux-router-engine

[daviddm-url]: https://david-dm.org/electrode-io/electrode?path=packages/electrode-redux-router-engine

[daviddm-dev-image]: https://david-dm.org/electrode-io/electrode/dev-status.svg?path=packages/electrode-redux-router-engine

[daviddm-dev-url]: https://david-dm.org/electrode-io/electrode?path=packages/electrode-redux-router-engine?type-dev

[npm-downloads-image]: https://img.shields.io/npm/dm/electrode-redux-router-engine.svg

[npm-downloads-url]: https://www.npmjs.com/package/electrode-redux-router-engine
259 changes: 156 additions & 103 deletions packages/electrode-redux-router-engine/lib/redux-router-engine.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
"use strict";

/* eslint-disable max-params,indent,global-require */
/* eslint-disable max-statements, max-params, prefer-spread, global-require, complexity */

const Path = require("path");
const assert = require("assert");
const optionalRequire = require("optional-require")(require);
const Promise = optionalRequire("bluebird", { message: false, default: global.Promise });
const React = optionalRequire("react");
const ReactDomServer = optionalRequire("react-dom/server");
const ReactRouter = require("react-router");
const Provider = require("react-redux").Provider;
const Path = require("path");
const { StaticRouter } = require("react-router-dom");
const { matchRoutes, renderRoutes } = require("react-router-config");
const { combineReducers, createStore } = require("redux");
const pkg = require("../package.json");
const util = require("./util");

const BAD_CHARS_REGEXP = /[<\u2028\u2029]/g;
const REPLACEMENTS_FOR_BAD_CHARS = {
Expand All @@ -19,161 +22,211 @@ const REPLACEMENTS_FOR_BAD_CHARS = {
};

function escapeBadChars(sourceString) {
return sourceString.replace(BAD_CHARS_REGEXP, (match) => REPLACEMENTS_FOR_BAD_CHARS[match]);
return sourceString.replace(BAD_CHARS_REGEXP, match => REPLACEMENTS_FOR_BAD_CHARS[match]);
}

const ROUTE_HANDLER = Symbol("route handler");

class ReduxRouterEngine {
constructor(options) {
assert(options.routes, "Must provide react-router routes for redux-router-engine");
assert(options.createReduxStore, "Must provide createReduxStore for redux-router-engine");

this.options = options;

this.options.withIds = !!options.withIds;

if (!options.stringifyPreloadedState) {
this.options.stringifyPreloadedState =
(state) => `window.__PRELOADED_STATE__ = ${escapeBadChars(JSON.stringify(state))};`;
this.options.stringifyPreloadedState = state =>
`window.__PRELOADED_STATE__ = ${escapeBadChars(JSON.stringify(state))};`;
}

if (!this.options.logError) {
this.options.logError = (req, err) =>
console.log("Electrode ReduxRouterEngine Error:", err); //eslint-disable-line
this.options.logError = (req, err) => console.log(`${pkg.name} Error:`, err); //eslint-disable-line
}

if (this.options.renderToString) {
this._renderToString = this.options.renderToString;
}

if (!this.options.routesHandlerPath) {
// Default for Electrode app
this.options.routesHandlerPath = Path.join(process.env.APP_SRC_DIR || "", "server/routes");
// if options.routes is a string, then treat it as a path to the routes source for require
if (typeof options.routes === "string") {
const x = util.resolveModulePath(options.routes);
this._routes = util.es6Default(require(x));
} else {
this._routes = options.routes;
}

this.options.routesHandlerPath = Path.resolve(this.options.routesHandlerPath);
this._routesDir = options.routesHandlerPath
? Path.resolve(options.routesHandlerPath)
: Path.resolve(process.env.APP_SRC_DIR || "", "server/routes");

this._handlers = {};
this._routesComponent = renderRoutes(this._routes);
}

render(req, options) {
async render(req, options) {
const location = req.path || (req.url && req.url.path);

return this._matchRoute({ routes: this.options.routes, location })
.then((match) => {
if (match.redirectLocation) {
return {
status: 302,
path: `${match.redirectLocation.pathname}${match.redirectLocation.search}`
};
}

if (!match.renderProps) {
return {
status: 404,
message: `redux-router-engine: Path ${location} not found`
};
}
const routes = match.renderProps.routes;
const route = routes[routes.length - 1];
const methods = route.methods || "get";

if (methods.toLowerCase().indexOf(req.method.toLowerCase()) < 0) {
throw new Error(
`redux-router-engine: ${location} doesn't allow request method ${req.method}`);
}

return this._handleRender(req, match, route, options || {});
})
.catch((err) => {
this.options.logError.call(this, req, err);
try {
const match = this._matchRoute(req, this._routes, location);

if (match.length === 0) {
return {
status: err.status || 500, // eslint-disable-line
message: err.message,
path: err.path,
_err: err
status: 404,
message: `${pkg.name}: Path ${location} not found`
};
});
}

// TODO: support redirect
// const redirect = match.find(x => x.redirect);
// if (redirect) {
// return {
// status: 302,
// path: `${redirect.redirect}`
// };
// }

const methods = match[0].methods || "get";

if (methods.toLowerCase().indexOf(req.method.toLowerCase()) < 0) {
throw new Error(`${pkg.name}: ${location} doesn't allow request method ${req.method}`);
}

return await this._handleRender(req, location, match, options || {});
} catch (err) {
this.options.logError.call(this, req, err);
return {
status: err.status || 500, // eslint-disable-line
message: err.message,
path: err.path || location,
_err: err
};
}
}

//
// options: { routes, location: url_path }
//
_matchRoute(options) {
return new Promise((resolve, reject) => {
ReactRouter.match(options, (err, redirectLocation, renderProps) => {
if (err) {
reject(err);
} else {
resolve({ redirectLocation, renderProps });
}
});
});
_matchRoute(req, routes, location) {
return matchRoutes(routes, location);
}

_handleRender(req, match, route, options) {
async _handleRender(req, location, match, options) {
const withIds = options.withIds !== undefined ? options.withIds : this.options.withIds;
const stringifyPreloadedState =
options.stringifyPreloadedState || this.options.stringifyPreloadedState;

return this._getReduxStoreInitializer(route, options).call(this, req, match)
.then((store) => {
const r = {};
const x = this._renderToString(req, store, match, withIds);
if (x.then !== undefined) { // a Promise?
return x.then((html) => {
r.status = 200;
r.html = html;
r.prefetch = stringifyPreloadedState(store.getState());
return r;
});
} else {
r.status = 200;
r.html = x;
r.prefetch = stringifyPreloadedState(store.getState());
return r;
}
});
const inits = [];

for (let ri = 1; ri < match.length; ri++) {
const route = match[ri].route;
const init = this._getRouteInit(route);
if (init) inits.push(init({ req, location, match, route, inits }));
}

let awaited = false;
const awaitInits = async () => {
if (awaited) return;
awaited = true;
for (let x = 0; x < inits.length; x++) {
if (inits[x].then) inits[x] = await inits[x];
}
};

let topInit = this._getRouteInit(match[0].route);
if (topInit) {
topInit = topInit({ req, location, match, route: match[0].route, inits, awaitInits });
}

if (topInit.then) {
await awaitInits();
topInit = await topInit;
}

let store;
if (topInit.store) {
// top route provided a ready made store, just use it
store = topInit.store;
} else {
if (!awaited) await awaitInits();

let reducer;
let initialState;

if (topInit.initialState || inits.length > 0) {
initialState = Object.assign.apply(
null,
[{}, topInit.initialState].concat(inits.map(x => x.initialState))
);
} else {
// no route provided any initialState
initialState = {};
}

if (typeof topInit.reducer === "function") {
// top route provided a ready made reducer
reducer = topInit.reducer;
} else if (topInit.reducer || inits.length > 0) {
// top route only provide its own reducer and initialState
const allReducers = Object.assign.apply(
null,
[{}, topInit.reducer].concat(inits.map(x => x.reducer))
);

reducer = combineReducers(allReducers);
} else {
// no route provided any reducer
reducer = x => x;
}

store = createStore(reducer, initialState);
}

let html = this._renderToString(req, location, store, match, withIds);
if (html.then !== undefined) {
// a Promise?
html = await html;
}

return { status: 200, html, prefetch: stringifyPreloadedState(store.getState()) };
}

_renderToString(req, store, match, withIds) { // eslint-disable-line
_renderToString(req, location, store, match, withIds) {
if (req.app && req.app.disableSSR) {
return "";
return "<!-- SSR disabled by request -->";
} else {
assert(React, "Can't do SSR because React module is not available");
assert(ReactDomServer, "Can't do SSR because ReactDomServer module is not available");
assert(React, `${pkg.name}: can't do SSR because react not found`);
assert(ReactDomServer, `${pkg.name}: can't do SSR because react-dom not found`);
const context = {};
return (withIds ? ReactDomServer.renderToString : ReactDomServer.renderToStaticMarkup)(
React.createElement(
Provider, { store },
React.createElement(ReactRouter.RouterContext, match.renderProps)
Provider,
{ store },
React.createElement(StaticRouter, { location, context }, this._routesComponent)
)
);
}
}

_getReduxStoreInitializer(route, options) {
let h = this._handlers[route.path];
if (h) {
return h;
_getRouteInit(route) {
let h = route[ROUTE_HANDLER];

if (h !== undefined) return h;

if (!route.init) {
h = false;
} else if (route.init === true) {
h = Path.join(this._routesDir, route.path);
} else {
assert(typeof route.init === "string", `${pkg.name}: route init prop must be a string`);
h = util.resolveModulePath(route.init, this._routesDir);
}

switch (route.init) {
case undefined:
h = options.createReduxStore || this.options.createReduxStore;
break;
case true:
h = require(Path.join(this.options.routesHandlerPath, route.path));
break;
default:
assert(typeof route.init === "string", "route init prop must be a string");
h = require(Path.join(this.options.routesHandlerPath, route.init));
break;
if (h) {
h = util.es6Default(require(h));
}

this._handlers[route.path] = h;
route[ROUTE_HANDLER] = h;

return h;
}

}

module.exports = ReduxRouterEngine;
Loading

0 comments on commit de76db2

Please sign in to comment.