From a5327546e28d5326be1036439d8debdcc08e0937 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Fri, 14 Dec 2018 13:47:33 -0600 Subject: [PATCH 1/5] ci(publish): enabled for all branches so that semantic-release can control which branches it publishes from --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 70609f7e..dc4d2d4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ deploy: provider: script skip_cleanup: true script: npx semantic-release@beta + on: + all_branches: true env: global: - FORCE_COLOR=1 From 7ff3fae31236d93dd87e9497e45ad5b0abe75071 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Fri, 14 Dec 2018 14:19:57 -0600 Subject: [PATCH 2/5] chore(rendered-content): nested html under rendered-content to make room for additional content from custom rendering functions BREAKING CHANGE: the shape of rendered content has changed from a string to an object. the previous string content is now provided as an `html` property of the object --- example/layout.mustache | 2 +- src/router-wrapper.js | 12 +++++++----- .../features/step_definitions/render-steps.js | 6 +++--- test/unit/router-wrapper-test.js | 6 +++--- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/example/layout.mustache b/example/layout.mustache index 797a1988..a6528a06 100644 --- a/example/layout.mustache +++ b/example/layout.mustache @@ -5,7 +5,7 @@ -
{{{ renderedContent }}}
+
{{{ renderedContent.html }}}
diff --git a/src/router-wrapper.js b/src/router-wrapper.js index 75302294..9762f8fe 100644 --- a/src/router-wrapper.js +++ b/src/router-wrapper.js @@ -27,11 +27,13 @@ export default async function renderThroughReactRouter(request, h, {routes, resp return respond(h, { store, status, - renderedContent: renderToString(( - - - - )) + renderedContent: { + html: renderToString(( + + + + )) + } }); } } catch (e) { diff --git a/test/integration/features/step_definitions/render-steps.js b/test/integration/features/step_definitions/render-steps.js index 5af7ac4a..d9f5b02d 100644 --- a/test/integration/features/step_definitions/render-steps.js +++ b/test/integration/features/step_definitions/render-steps.js @@ -2,18 +2,18 @@ import {OK} from 'http-status-codes'; import {assert} from 'chai'; import {When, Then} from 'cucumber'; -When('a request is made for an existing route', function () { +When(/^a request is made for an existing route$/, function () { return this.makeRequest({url: '/existing-route'}); }); -Then('the route is rendered successfully', function (callback) { +Then(/^the route is rendered successfully$/, function (callback) { assert.equal(this.serverResponse.statusCode, OK); assert.equal(this.serverResponse.headers['content-type'], 'text/html; charset=utf-8'); callback(); }); -Then('asynchronously fetched data is included in the page', function (callback) { +Then(/^asynchronously fetched data is included in the page$/, function (callback) { assert.include(this.serverResponse.payload, this.dataPoint); callback(); diff --git a/test/unit/router-wrapper-test.js b/test/unit/router-wrapper-test.js index c28fd627..08836f90 100644 --- a/test/unit/router-wrapper-test.js +++ b/test/unit/router-wrapper-test.js @@ -38,14 +38,14 @@ suite('router-wrapper', () => { const status = any.integer(); const context = any.simpleObject(); const rootComponent = any.simpleObject(); - const renderedContent = any.string(); + const html = any.string(); const response = any.string(); routeMatcher.default.withArgs(url, routes).resolves({renderProps, status}); dataFetcher.default.withArgs({renderProps, store, status}).resolves({renderProps, status}); React.createElement.withArgs(RouterContext, sinon.match(renderProps)).returns(context); React.createElement.withArgs(Root, {request, store}).returns(rootComponent); - domServer.renderToString.withArgs(rootComponent).returns(renderedContent); - respond.withArgs(reply, {renderedContent, store, status}).returns(response); + domServer.renderToString.withArgs(rootComponent).returns(html); + respond.withArgs(reply, {renderedContent: {html}, store, status}).returns(response); return assert.becomes(renderThroughReactRouter(request, reply, {routes, respond, Root, store}), response); }); From ec1f631adb8aff66dec5d7374fde7d93b3ba66b5 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Mon, 17 Dec 2018 11:09:17 -0600 Subject: [PATCH 3/5] WIP(default-render): extracted the default render function --- src/default-render-factory.js | 11 ++++++ test/unit/default-render-factory-test.js | 47 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/default-render-factory.js create mode 100644 test/unit/default-render-factory-test.js diff --git a/src/default-render-factory.js b/src/default-render-factory.js new file mode 100644 index 00000000..3f336099 --- /dev/null +++ b/src/default-render-factory.js @@ -0,0 +1,11 @@ +import React from 'react'; +import {renderToString} from 'react-dom/server'; +import {RouterContext} from 'react-router'; + +export default function (request, store, renderProps, Root) { + return otherProps => renderToString( + + + + ); +} diff --git a/test/unit/default-render-factory-test.js b/test/unit/default-render-factory-test.js new file mode 100644 index 00000000..f7a84052 --- /dev/null +++ b/test/unit/default-render-factory-test.js @@ -0,0 +1,47 @@ +import React from 'react'; +import domServer from 'react-dom/server'; +import {RouterContext} from 'react-router'; +import sinon from 'sinon'; +import any from '@travi/any'; +import {assert} from 'chai'; +import defaultRenderFactory from '../../src/default-render-factory'; + +suite('default-render factory', () => { + let sandbox; + const Root = any.simpleObject(); + const store = any.simpleObject(); + + setup(() => { + sandbox = sinon.createSandbox(); + + sandbox.stub(React, 'createElement'); + sandbox.stub(domServer, 'renderToString'); + }); + + teardown(() => sandbox.restore()); + + test('that the router-context is rendered within the provided root component', () => { + const renderProps = any.simpleObject(); + const rootComponent = any.simpleObject(); + const html = any.string(); + const request = any.simpleObject(); + React.createElement.withArgs(RouterContext, renderProps).returns(context); + React.createElement.withArgs(Root, {request, store}, context).returns(rootComponent); + domServer.renderToString.withArgs(rootComponent).returns(html); + + assert.equal(defaultRenderFactory(request, store, renderProps, Root)(), html); + }); + + test('that the additional props are passed to the root component, when provided', () => { + const renderProps = any.simpleObject(); + const rootComponent = any.simpleObject(); + const html = any.string(); + const request = any.simpleObject(); + const otherProps = any.simpleObject(); + React.createElement.withArgs(RouterContext, renderProps).returns(context); + React.createElement.withArgs(Root, {request, store, ...otherProps}, context).returns(rootComponent); + domServer.renderToString.withArgs(rootComponent).returns(html); + + assert.equal(defaultRenderFactory(request, store, renderProps, Root)(otherProps), html); + }); +}); From 345b4c883e728c1757e9e8073cb835960cddb456 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Mon, 17 Dec 2018 11:11:18 -0600 Subject: [PATCH 4/5] WIP(custom-render): passed the custom renderer to the wrapper --- src/route.js | 1 + test/unit/route-test.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/route.js b/src/route.js index bfb618e3..9bc1f6ce 100644 --- a/src/route.js +++ b/src/route.js @@ -8,6 +8,7 @@ export const plugin = { method: 'GET', path: '/html', handler: (request, h) => renderThroughReactRouter(request, h, { + render: options.render, routes: options.routes, respond: options.respond, Root: options.Root, diff --git a/test/unit/route-test.js b/test/unit/route-test.js index 348b26c0..24699cf6 100644 --- a/test/unit/route-test.js +++ b/test/unit/route-test.js @@ -20,6 +20,7 @@ suite('route', () => { }); test('that the request for html is handled', async () => { + const render = () => undefined; const route = sinon.stub(); const respond = sinon.spy(); const routes = sinon.spy(); @@ -32,7 +33,7 @@ suite('route', () => { const configureStore = sinon.stub(); configureStore.withArgs({session: {auth: auth.credentials}, server}).returns(store); - await plugin.register(server, {respond, routes, Root, configureStore}); + await plugin.register(server, {render, respond, routes, Root, configureStore}); assert.calledWith(route, sinon.match({ method: 'GET', @@ -41,6 +42,6 @@ suite('route', () => { route.yieldTo('handler', request, reply); - assert.calledWith(routerWrapper.default, request, reply, {routes, respond, Root, store}); + assert.calledWith(routerWrapper.default, request, reply, {render, routes, respond, Root, store}); }); }); From 86298c3519edf5fb25f94c261a805f65a7f62c69 Mon Sep 17 00:00:00 2001 From: Matt Travi Date: Mon, 17 Dec 2018 11:26:20 -0600 Subject: [PATCH 5/5] feat(custom-render): called a custom renderer when provided --- src/router-wrapper.js | 16 +++++---------- test/unit/router-wrapper-test.js | 35 ++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/router-wrapper.js b/src/router-wrapper.js index 9762f8fe..7a48446f 100644 --- a/src/router-wrapper.js +++ b/src/router-wrapper.js @@ -1,12 +1,10 @@ -import React from 'react'; -import {renderToString} from 'react-dom/server'; -import {RouterContext} from 'react-router'; import Boom from 'boom'; import {MOVED_PERMANENTLY, MOVED_TEMPORARILY} from 'http-status-codes'; import matchRoute from './route-matcher'; import fetchData from './data-fetcher'; +import defaultRenderFactory from './default-render-factory'; -export default async function renderThroughReactRouter(request, h, {routes, respond, Root, store}) { +export default async function renderThroughReactRouter(request, h, {render, routes, respond, Root, store}) { try { const {renderProps, status, redirectLocation} = await matchRoute(request.raw.req.url, routes); @@ -24,16 +22,12 @@ export default async function renderThroughReactRouter(request, h, {routes, resp } else { await fetchData({renderProps, store}); + const defaultRender = defaultRenderFactory(request, store, renderProps, Root); + return respond(h, { store, status, - renderedContent: { - html: renderToString(( - - - - )) - } + renderedContent: render ? render(defaultRender) : {html: defaultRender()} }); } } catch (e) { diff --git a/test/unit/router-wrapper-test.js b/test/unit/router-wrapper-test.js index 08836f90..d71c15b8 100644 --- a/test/unit/router-wrapper-test.js +++ b/test/unit/router-wrapper-test.js @@ -1,12 +1,10 @@ -import React from 'react'; -import {RouterContext} from 'react-router'; -import domServer from 'react-dom/server'; -import {MOVED_TEMPORARILY, MOVED_PERMANENTLY} from 'http-status-codes'; +import {MOVED_PERMANENTLY, MOVED_TEMPORARILY} from 'http-status-codes'; import sinon from 'sinon'; import {assert} from 'chai'; import any from '@travi/any'; import Boom from 'boom'; import renderThroughReactRouter from '../../src/router-wrapper'; +import * as defaultRenderFactory from '../../src/default-render-factory'; import * as routeMatcher from '../../src/route-matcher'; import * as dataFetcher from '../../src/data-fetcher'; @@ -25,8 +23,7 @@ suite('router-wrapper', () => { sandbox.stub(routeMatcher, 'default'); sandbox.stub(dataFetcher, 'default'); sandbox.stub(Boom, 'wrap'); - sandbox.stub(React, 'createElement'); - sandbox.stub(domServer, 'renderToString'); + sandbox.stub(defaultRenderFactory, 'default'); }); teardown(() => sandbox.restore()); @@ -36,20 +33,36 @@ suite('router-wrapper', () => { const reply = sinon.spy(); const renderProps = any.simpleObject(); const status = any.integer(); - const context = any.simpleObject(); - const rootComponent = any.simpleObject(); const html = any.string(); const response = any.string(); + const defaultRender = sinon.stub(); routeMatcher.default.withArgs(url, routes).resolves({renderProps, status}); dataFetcher.default.withArgs({renderProps, store, status}).resolves({renderProps, status}); - React.createElement.withArgs(RouterContext, sinon.match(renderProps)).returns(context); - React.createElement.withArgs(Root, {request, store}).returns(rootComponent); - domServer.renderToString.withArgs(rootComponent).returns(html); + defaultRender.returns(html); + defaultRenderFactory.default.withArgs(request, store, renderProps, Root).returns(defaultRender); respond.withArgs(reply, {renderedContent: {html}, store, status}).returns(response); return assert.becomes(renderThroughReactRouter(request, reply, {routes, respond, Root, store}), response); }); + test('that response contains the custom-rendered content when a custom renderer is provided', async () => { + const respond = sinon.stub(); + const reply = sinon.spy(); + const renderProps = any.simpleObject(); + const status = any.integer(); + const response = any.string(); + const render = sinon.stub(); + const renderedContent = any.simpleObject(); + const defaultRender = () => undefined; + routeMatcher.default.withArgs(url, routes).resolves({renderProps, status}); + dataFetcher.default.withArgs({renderProps, store, status}).resolves({renderProps, status}); + respond.withArgs(reply, {renderedContent, store, status}).returns(response); + defaultRenderFactory.default.withArgs(request, store, renderProps, Root).returns(defaultRender); + render.withArgs(defaultRender).returns(renderedContent); + + assert.equal(await renderThroughReactRouter(request, reply, {render, routes, respond, Root, store}), response); + }); + test('that a temporary redirect results when a redirectLocation is defined with a 302 status', () => { const respond = sinon.stub(); const redirect = sinon.stub();