diff --git a/.babelrc b/.babelrc index e4ff861..93fbaa3 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - presets: ["es2015", "stage-0", "react"] -} + "presets": ["es2015", "stage-0", "react"] +} \ No newline at end of file diff --git a/.npmignore b/.npmignore index feb2233..df8475e 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,3 @@ - # Created by https://www.gitignore.io/api/gitbook,osx,webstorm,node ### GitBook ### @@ -141,4 +140,4 @@ dist /.npmignore /xclap.js /.babelrc -/test +/test \ No newline at end of file diff --git a/README.md b/README.md index 9938c8f..cfd2ce8 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# electrode-apollo-redux +# electrode-apollo-redux-engine diff --git a/index.js b/index.js new file mode 100644 index 0000000..cfa64c8 --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports.combineReducerWithApollo = require("./lib/combineReducerWithApollo"); diff --git a/lib/apollo-redux-engine.js b/lib/apollo-redux-engine.js deleted file mode 100644 index 2c1cecc..0000000 --- a/lib/apollo-redux-engine.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; - -const assert = require("assert"); -const optionalRequire = require("optional-require")(require); -const React = optionalRequire("react"); -const ReactDomServer = optionalRequire("react-dom/server"); -const ReactRouter = require("react-router"); -const Provider = require("react-redux").Provider; -const ApolloProvider = require("react-apollo").ApolloProvider; - -const combineReducerWithApollo = (reducer, client, key = "apollo") => { - const apolloReducer = client.reducer(); - - return function combination(state = {}, action) { - // Remove appolo key to prevent unexpected key warning. - const apolloState = state[key]; - delete state[key]; - - const nextState = reducer(state, action); - - const previousStateForApollo = apolloState; - const nextStateForApollo = apolloReducer(previousStateForApollo, action); - - if (previousStateForApollo !== nextStateForApollo) { - return Object.assign({}, nextState, {[key]: nextStateForApollo}); - } else { - nextState[key] = previousStateForApollo; - return nextState; - } - }; -}; - -const renderToString = (req, store, match, withIds) => { // eslint-disable-line - if (req.app && req.app.disableSSR) { - return ""; - } 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"); - - const app = req.server && req.server.app || req.app; - if (app.apollo) { - return (withIds ? ReactDomServer.renderToString : ReactDomServer.renderToStaticMarkup)( - React.createElement( - ApolloProvider, { store, client: app.apollo }, - React.createElement(ReactRouter.RouterContext, match.renderProps) - ) - ); - } else { - return (withIds ? ReactDomServer.renderToString : ReactDomServer.renderToStaticMarkup)( - React.createElement( - Provider, { store }, - React.createElement(ReactRouter.RouterContext, match.renderProps) - ) - ); - } - } -}; - -module.exports = { - combineReducerWithApollo, - renderToString -}; diff --git a/lib/combineReducerWithApollo.js b/lib/combineReducerWithApollo.js new file mode 100644 index 0000000..779e106 --- /dev/null +++ b/lib/combineReducerWithApollo.js @@ -0,0 +1,25 @@ +"use strict"; + +const combineReducerWithApollo = (reducer, client, key = "apollo") => { + const apolloReducer = client.reducer(); + + return function combination(state = {}, action) { + // Remove appolo key to prevent unexpected key warning. + const apolloState = state[key]; + delete state[key]; + + const nextState = reducer(state, action); + + const previousStateForApollo = apolloState; + const nextStateForApollo = apolloReducer(previousStateForApollo, action); + + if (previousStateForApollo !== nextStateForApollo) { + return Object.assign({}, nextState, {[key]: nextStateForApollo}); + } else { + nextState[key] = previousStateForApollo; + return nextState; + } + }; +}; + +module.exports = combineReducerWithApollo; diff --git a/lib/renderToString.js b/lib/renderToString.js new file mode 100644 index 0000000..c7cce6b --- /dev/null +++ b/lib/renderToString.js @@ -0,0 +1,39 @@ +"use strict"; + +const assert = require("assert"); +const optionalRequire = require("optional-require")(require); +const React = optionalRequire("react"); +const ReactDomServer = optionalRequire("react-dom/server"); +const ReactApollo = optionalRequire("react-apollo"); +const ReactRouter = require("react-router"); +const Provider = require("react-redux").Provider; + +const renderToString = (req, store, match, withIds) => { // eslint-disable-line + if (req.app && req.app.disableSSR) { + return ""; + } 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"); + + const app = req.server && req.server.app || req.app; + if (app.apollo) { + assert(ReactApollo, "Can't use Apollo because ReactApollo module is not available"); + + return (withIds ? ReactDomServer.renderToString : ReactDomServer.renderToStaticMarkup)( + React.createElement( + ReactApollo.ApolloProvider, { store, client: app.apollo }, + React.createElement(ReactRouter.RouterContext, match.renderProps) + ) + ); + } else { + return (withIds ? ReactDomServer.renderToString : ReactDomServer.renderToStaticMarkup)( + React.createElement( + Provider, { store }, + React.createElement(ReactRouter.RouterContext, match.renderProps) + ) + ); + } + } +}; + +module.exports = renderToString; diff --git a/package.json b/package.json index 08fdfc0..29553e0 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "electrode-apollo-redux-engine", - "version": "1.0.1", + "version": "1.0.3", "description": "Integrating Electrode with Apollo and Redux", - "main": "lib/apollo-redux-engine.js", + "main": "index.js", "scripts": { "test": "clap test", "lint": "clap lint", @@ -10,7 +10,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/khayong/electrode-apollo-redux.git" + "url": "git+https://github.com/khayong/electrode-apollo-redux-engine.git" }, "keywords": [ "Electrode", @@ -20,27 +20,29 @@ "author": "Tan Khay Ong (https://github.com/khayong)", "license": "MIT", "bugs": { - "url": "https://github.com/khayong/electrode-apollo-redux/issues" + "url": "https://github.com/khayong/electrode-apollo-redux-engine/issues" }, - "homepage": "https://github.com/khayong/electrode-apollo-redux#readme", + "homepage": "https://github.com/khayong/electrode-apollo-redux-engine#readme", "devDependencies": { - "babel-cli": "^6.24.1", - "babel-preset-es2015": "^6.24.1", - "babel-preset-stage-0": "^6.24.1", - "babel-register": "^6.24.1", + "babel-preset-es2015": "^6.0.15", + "babel-preset-react": "^6.0.15", + "babel-preset-stage-0": "^6.0.15", + "babel-register": "^6.5.2", "electrode-archetype-njs-module-dev": "^2.2.0", - "electrode-redux-router-engine": "^1.4.5" + "electrode-redux-router-engine": "^1.4.5", + "react": "^15.6.1", + "react-apollo": "^1.4.3", + "redux": "^3.7.2" }, "dependencies": { "optional-require": "^1.0.0", - "react-apollo": "^1.4.3", - "react-dom": "^15.6.1", "react-redux": "^5.0.5", "react-router": "^3.0.5" }, "peerDependencies": { - "react": "^15.3.1 || ^0.14.8", - "react-dom": "^15.3.1 || ^0.14.8" + "react": "^15.6.1", + "react-apollo": "^1.4.3", + "react-dom": "^15.6.1" }, "nyc": { "all": true, diff --git a/server.js b/server.js new file mode 100644 index 0000000..038bea7 --- /dev/null +++ b/server.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports.renderToString = require("./lib/renderToString"); \ No newline at end of file diff --git a/test/spec/apollo-redux-engine.spec.js b/test/spec/apollo-redux-engine.spec.js deleted file mode 100644 index 4bf36b0..0000000 --- a/test/spec/apollo-redux-engine.spec.js +++ /dev/null @@ -1,102 +0,0 @@ -"use strict"; - -const createStore = require("redux").createStore; -const combineReducers = require("redux").combineReducers; -const ApolloClient = require("react-apollo").ApolloClient; -//const ReduxRouterEngine = require("electrode-redux-router-engine").default; -const combineReducerWithApollo = require("../..").combineReducerWithApollo; -//const renderToString = require("../..").renderToString; - -require("babel-register"); - -//const routes = require("../routes.jsx").default; - -const checkBox = (store, action) => { - if (action.type === "TOGGLE_CHECK") { - return { - checked: !store.checked - }; - } - - return store || {checked: false}; -}; - -const number = (store, action) => { - if (action.type === "INC_NUMBER") { - return { - value: store.value + 1 - }; - } else if (action.type === "DEC_NUMBER") { - return { - value: store.value - 1 - }; - } - - return store || {value: 0}; -}; - -describe("apollo-redux-engine", function () { - - it("should combine existing root reducer", () => { - const client = new ApolloClient(); - - const rootReducer = combineReducers({ - checkBox, - number - }); - - const initialState = { - checkBox: {checked: false}, - number: {value: 999} - }; - const store = createStore(combineReducerWithApollo(rootReducer, client), initialState); - expect(store.getState()).to.have.all.keys("checkBox", "number", "apollo"); - expect(store.getState().checkBox).to.deep.equal({checked: false}); - expect(store.getState().number).to.deep.equal({value: 999}); - - store.dispatch({type: "TOGGLE_CHECK"}); - expect(store.getState().checkBox).to.deep.equal({checked: true}); - store.dispatch({type: "TOGGLE_CHECK"}); - expect(store.getState().checkBox).to.deep.equal({checked: false}); - - store.dispatch({type: "INC_NUMBER"}); - expect(store.getState().number).to.deep.equal({value: 1000}); - store.dispatch({type: "DEC_NUMBER"}); - expect(store.getState().number).to.deep.equal({value: 999}); - }); - - /*it("should render to string without apollo", () => { - const rootReducer = combineReducers({ - checkBox, - number - }); - - const createReduxStore = (req, match) => { // eslint-disable-line - const initialState = { - checkBox: {checked: false}, - number: {value: 999} - }; - - const store = createStore(rootReducer, initialState); - return Promise.resolve(store); - }; - - const testReq = { - method: "get", - log: () => { - }, - app: {}, - url: { - path: "/test" - } - }; - - console.log(new ReduxRouterEngine({routes, createReduxStore, renderToString})); - return engine.render(testReq).then(result => { - console.log(result); - }); - //console.log(renderToString(testReq, store, match)); - - - });*/ -}); diff --git a/test/spec/combine.spec.js b/test/spec/combine.spec.js new file mode 100644 index 0000000..684cfce --- /dev/null +++ b/test/spec/combine.spec.js @@ -0,0 +1,62 @@ +"use strict"; + +const createStore = require("redux").createStore; +const combineReducers = require("redux").combineReducers; +const ApolloClient = require("react-apollo").ApolloClient; + +const {combineReducerWithApollo} = require("../.."); + +const checkBox = (store, action) => { + if (action.type === "TOGGLE_CHECK") { + return { + checked: !store.checked + }; + } + + return store || {checked: false}; +}; + +const number = (store, action) => { + if (action.type === "INC_NUMBER") { + return { + value: store.value + 1 + }; + } else if (action.type === "DEC_NUMBER") { + return { + value: store.value - 1 + }; + } + + return store || {value: 0}; +}; + +describe("Combine reducer with Apollo", function () { + + it("should combine existing root reducer", () => { + const client = new ApolloClient(); + + const rootReducer = combineReducers({ + checkBox, + number + }); + + const initialState = { + checkBox: {checked: false}, + number: {value: 999} + }; + const store = createStore(combineReducerWithApollo(rootReducer, client), initialState); + expect(store.getState()).to.have.all.keys("checkBox", "number", "apollo"); + expect(store.getState().checkBox).to.deep.equal({checked: false}); + expect(store.getState().number).to.deep.equal({value: 999}); + + store.dispatch({type: "TOGGLE_CHECK"}); + expect(store.getState().checkBox).to.deep.equal({checked: true}); + store.dispatch({type: "TOGGLE_CHECK"}); + expect(store.getState().checkBox).to.deep.equal({checked: false}); + + store.dispatch({type: "INC_NUMBER"}); + expect(store.getState().number).to.deep.equal({value: 1000}); + store.dispatch({type: "DEC_NUMBER"}); + expect(store.getState().number).to.deep.equal({value: 999}); + }); +}); diff --git a/test/spec/render.spec.js b/test/spec/render.spec.js new file mode 100644 index 0000000..b24eb90 --- /dev/null +++ b/test/spec/render.spec.js @@ -0,0 +1,107 @@ +"use strict"; + +const createStore = require("redux").createStore; +const combineReducers = require("redux").combineReducers; +const ApolloClient = require("react-apollo").ApolloClient; +const ReduxRouterEngine = require("electrode-redux-router-engine"); + +const {combineReducerWithApollo} = require("../.."); +const {renderToString} = require("../../server"); + +require("babel-register"); + +const routes = require("../routes.jsx").default; + +const checkBox = (store, action) => { + if (action.type === "TOGGLE_CHECK") { + return { + checked: !store.checked + }; + } + + return store || {checked: false}; +}; + +const number = (store, action) => { + if (action.type === "INC_NUMBER") { + return { + value: store.value + 1 + }; + } else if (action.type === "DEC_NUMBER") { + return { + value: store.value - 1 + }; + } + + return store || {value: 0}; +}; + +const getPrefetchState = (prefetch) => { + const regex = /^window.__PRELOADED_STATE__ = (.*);$/g; + return JSON.parse(regex.exec(prefetch)[1]); +}; + +describe("Server rendering with Apollo", function () { + + it("should render to string without apollo", () => { + const rootReducer = combineReducers({ + checkBox, + number + }); + const createReduxStore = (req, match) => { // eslint-disable-line + const initialState = { + checkBox: {checked: false}, + number: {value: 999} + }; + const store = createStore(rootReducer, initialState); + return Promise.resolve(store); + }; + const testReq = { + method: "get", + log: () => { + }, + app: {}, + url: { + path: "/test" + } + }; + + const engine = new ReduxRouterEngine({routes, createReduxStore, renderToString}); + return engine.render(testReq).then(result => { + expect(getPrefetchState(result.prefetch)).to.deep.equal({checkBox: {checked: false}, number: {value: 999}}); + }); + }); + + it("should render to string with apollo", () => { + const client = new ApolloClient(); + + const rootReducer = combineReducers({ + checkBox, + number + }); + const createReduxStore = (req, match) => { // eslint-disable-line + const initialState = { + checkBox: {checked: false}, + number: {value: 999} + }; + const store = createStore(combineReducerWithApollo(rootReducer, client), initialState); + return Promise.resolve(store); + }; + const testReq = { + method: "get", + log: () => { + }, + app: { + apollo: client + }, + url: { + path: "/test" + } + }; + + const engine = new ReduxRouterEngine({routes, createReduxStore, renderToString}); + return engine.render(testReq).then(result => { + expect(getPrefetchState(result.prefetch)).to.have.all.keys(["checkBox", "number", "apollo"]); + }); + }); +});