From c0736dfe6aff8aec429720d787344b10c7dca6a0 Mon Sep 17 00:00:00 2001 From: Ben Scott Date: Thu, 22 Oct 2020 14:39:17 -0700 Subject: [PATCH 1/2] Support configuring Html props from react-server react-html's component supports a bunch of props for configuring content to inject into the head, however previously that was not exposed from react-server's createRender and createServer functions. This commit lets you pass a htmlProps option into createRender and createServer that will then be applied to the Html element. This means you can use createServer, and inject arbitary content into the head using 's headMarkup prop (and use any of 's other props too) --- packages/react-html/CHANGELOG.md | 2 + .../react-html/src/server/components/Html.tsx | 8 +-- .../react-html/src/server/components/index.ts | 2 +- packages/react-server/CHANGELOG.md | 2 + packages/react-server/README.md | 3 + packages/react-server/src/render/index.ts | 2 +- packages/react-server/src/render/render.tsx | 29 +++++++++- .../src/render/test/render.test.tsx | 57 ++++++++++++++++++- packages/react-server/src/server/server.ts | 13 +++-- .../src/server/test/server.test.tsx | 31 ++++++++++ 10 files changed, 133 insertions(+), 16 deletions(-) diff --git a/packages/react-html/CHANGELOG.md b/packages/react-html/CHANGELOG.md index 3a53f74213..75a4054e16 100644 --- a/packages/react-html/CHANGELOG.md +++ b/packages/react-html/CHANGELOG.md @@ -7,6 +7,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +- Update `HtmlProps` to mark children as optional (same as any React component) and export it ([#1661](https://github.com/Shopify/quilt/pull/1661)) + ## [10.0.1] - 2020-10-20 - Updated `tslib` dependency to `^1.14.1`. [#1657](https://github.com/Shopify/quilt/pull/1657) diff --git a/packages/react-html/src/server/components/Html.tsx b/packages/react-html/src/server/components/Html.tsx index ec293e12a4..8ead61444f 100644 --- a/packages/react-html/src/server/components/Html.tsx +++ b/packages/react-html/src/server/components/Html.tsx @@ -20,10 +20,10 @@ export interface InlineStyle { content: string; } -export interface Props { +export interface HtmlProps { manager?: HtmlManager; hydrationManager?: HydrationManager; - children: React.ReactElement | string; + children?: React.ReactElement | string; locale?: string; styles?: Asset[]; inlineStyles?: InlineStyle[]; @@ -37,7 +37,7 @@ export interface Props { export default function Html({ manager, hydrationManager, - children, + children = '', locale = 'en', blockingScripts = [], scripts = [], @@ -46,7 +46,7 @@ export default function Html({ preloadAssets = [], headMarkup = null, bodyMarkup = null, -}: Props) { +}: HtmlProps) { const markup = typeof children === 'string' ? children diff --git a/packages/react-html/src/server/components/index.ts b/packages/react-html/src/server/components/index.ts index 29273d311d..5f3d341031 100644 --- a/packages/react-html/src/server/components/index.ts +++ b/packages/react-html/src/server/components/index.ts @@ -1,2 +1,2 @@ export {default as Serialize} from './Serialize'; -export {default as Html} from './Html'; +export {default as Html, HtmlProps} from './Html'; diff --git a/packages/react-server/CHANGELOG.md b/packages/react-server/CHANGELOG.md index d349cdc1fd..a88c07de2e 100644 --- a/packages/react-server/CHANGELOG.md +++ b/packages/react-server/CHANGELOG.md @@ -7,6 +7,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +- Add `htmlProps` to the options for `createRender` and `createServer`, these props will be passed into the call to `@shopify/react-html`'s `` component ([#1661](https://github.com/Shopify/quilt/pull/1661)) + ## [0.18.4] - 2020-10-20 - Added `tslib@^1.14.1` in the list of dependencies. [#1657](https://github.com/Shopify/quilt/pull/1657) diff --git a/packages/react-server/README.md b/packages/react-server/README.md index 9decbd1bec..0423e09ed4 100644 --- a/packages/react-server/README.md +++ b/packages/react-server/README.md @@ -87,6 +87,9 @@ interface Options { debug?: boolean; // a function similar to the render option but specifically used to render error pages for production SSR errors renderError: RenderFunction; + // additional props to pass into the Html component, or a function that takes a Koa.Context and returns a props object. + // See https://github.com/Shopify/quilt/blob/master/packages/react-html/README.md#html- + htmlProps?: HtmlProps | (ctx: Context) => HtmlProps } ``` diff --git a/packages/react-server/src/render/index.ts b/packages/react-server/src/render/index.ts index 0e99c059eb..28a9092ee5 100644 --- a/packages/react-server/src/render/index.ts +++ b/packages/react-server/src/render/index.ts @@ -1 +1 @@ -export {createRender, Context, RenderFunction} from './render'; +export {createRender, Context, RenderFunction, RenderOptions} from './render'; diff --git a/packages/react-server/src/render/render.tsx b/packages/react-server/src/render/render.tsx index b63033ab1a..76518f46fb 100644 --- a/packages/react-server/src/render/render.tsx +++ b/packages/react-server/src/render/render.tsx @@ -6,6 +6,7 @@ import {Context} from 'koa'; import compose from 'koa-compose'; import { Html, + HtmlProps, HtmlManager, HtmlContext, stream, @@ -46,13 +47,14 @@ interface Data { value: {[key: string]: any} | undefined; } -type Options = Pick< +export type RenderOptions = Pick< NonNullable>, 'afterEachPass' | 'betweenEachPass' > & { assetPrefix?: string; assetName?: string | ValueFromContext; renderError?: RenderFunction; + htmlProps?: HtmlProps | ValueFromContext; }; /** @@ -60,12 +62,16 @@ type Options = Pick< * @param render * @param options */ -export function createRender(render: RenderFunction, options: Options = {}) { +export function createRender( + render: RenderFunction, + options: RenderOptions = {}, +) { const manifestPath = getManifestPath(process.cwd()); const { assetPrefix, assetName: assetNameInput = 'main', renderError, + htmlProps: htmlPropsInput = {}, } = options; async function renderFunction(ctx: Context) { @@ -74,6 +80,15 @@ export function createRender(render: RenderFunction, options: Options = {}) { ? assetNameInput(ctx) : assetNameInput; + const { + scripts: additionalScripts = [], + styles: additionalStyles = [], + ...additionalHtmlProps + } = + typeof htmlPropsInput === 'function' + ? htmlPropsInput(ctx) + : htmlPropsInput; + const logger = getLogger(ctx) || console; const assets = getAssets(ctx); @@ -133,8 +148,16 @@ export function createRender(render: RenderFunction, options: Options = {}) { assets.scripts({name: assetName, asyncAssets: immediateAsyncAssets}), ]); + styles.push(...additionalStyles); + scripts.push(...additionalScripts); + const response = stream( - + {app} , ); diff --git a/packages/react-server/src/render/test/render.test.tsx b/packages/react-server/src/render/test/render.test.tsx index 5cc9947ef7..e100908a29 100644 --- a/packages/react-server/src/render/test/render.test.tsx +++ b/packages/react-server/src/render/test/render.test.tsx @@ -7,8 +7,8 @@ import withEnv from '@shopify/with-env'; import {createRender, Context} from '../render'; import {mockMiddleware} from '../../test/utilities'; -const mockAssetsScripts = jest.fn(() => Promise.resolve([])); -const mockAssetsStyles = jest.fn(() => Promise.resolve([])); +const mockAssetsScripts = jest.fn(() => Promise.resolve([{path: 'main.js'}])); +const mockAssetsStyles = jest.fn(() => Promise.resolve([{path: 'main.css'}])); jest.mock('@shopify/sewing-kit-koa', () => ({ middleware: jest.fn(() => mockMiddleware), @@ -36,6 +36,59 @@ describe('createRender', () => { expect(await readStream(ctx.body)).toContain(myCoolApp); }); + it.each([ + [ + 'as Plain object', + { + scripts: [{path: '/extraScript.js'}], + styles: [{path: '/extraStyle.css'}], + headMarkup: , + }, + ], + [ + 'as Function', + () => ({ + scripts: [{path: '/extraScript.js'}], + styles: [{path: '/extraStyle.css'}], + headMarkup: , + }), + ], + ])( + 'response contains data passed in through htmlProps (%s)', + async (style, htmlProps) => { + const myCoolApp = 'My cool app'; + const ctx = createMockContext(); + + const renderFunction = createRender(() => <>{myCoolApp}, { + htmlProps, + }); + await renderFunction(ctx, noop); + + const bodyResult = await readStream(ctx.body); + + // Assets from manifest are still present + expect(bodyResult).toContain( + '', + ); + expect(bodyResult).toContain( + '', + ); + + // Additional script/style assets are added + expect(bodyResult).toContain( + '', + ); + expect(bodyResult).toContain( + '', + ); + + // Other props work + expect(bodyResult).toContain( + '', + ); + }, + ); + it('response contains x-quilt-data from headers', async () => { const myCoolApp = 'My cool app'; const data = {foo: 'bar'}; diff --git a/packages/react-server/src/server/server.ts b/packages/react-server/src/server/server.ts index dc7401fd99..b10e1477e3 100644 --- a/packages/react-server/src/server/server.ts +++ b/packages/react-server/src/server/server.ts @@ -5,11 +5,10 @@ import Koa, {Context} from 'koa'; import compose from 'koa-compose'; import mount from 'koa-mount'; -import {createRender, RenderFunction} from '../render'; +import {createRender, RenderFunction, RenderOptions} from '../render'; import {requestLogger} from '../logger'; import {metricsMiddleware as metrics} from '../metrics'; import {ping} from '../ping'; -import {ValueFromContext} from '../types'; const logger = console; @@ -18,10 +17,11 @@ interface Options { port?: number; assetPrefix?: string; proxy?: boolean; - assetName?: string | ValueFromContext; + assetName?: RenderOptions['assetName']; + htmlProps?: RenderOptions['htmlProps']; serverMiddleware?: compose.Middleware[]; render: RenderFunction; - renderError?: RenderFunction; + renderError?: RenderOptions['renderError']; app?: Koa; } @@ -50,6 +50,7 @@ export function createServer(options: Options): Server { renderError, serverMiddleware, assetName, + htmlProps, proxy = false, app = new Koa(), } = options; @@ -65,7 +66,9 @@ export function createServer(options: Options): Server { app.use(compose(serverMiddleware)); } - app.use(createRender(render, {assetPrefix, assetName, renderError})); + app.use( + createRender(render, {assetPrefix, assetName, renderError, htmlProps}), + ); return app.listen(port, ip, () => { logger.log(`started react-server on ${ip}:${port}`); diff --git a/packages/react-server/src/server/test/server.test.tsx b/packages/react-server/src/server/test/server.test.tsx index 26127d989b..245136e0d3 100644 --- a/packages/react-server/src/server/test/server.test.tsx +++ b/packages/react-server/src/server/test/server.test.tsx @@ -53,6 +53,37 @@ describe('createServer()', () => { ); }); + it('passes htmlProps through to the Html component', async () => { + function MockApp() { + return
markup
; + } + + const wrapper = await saddle((port, host) => + createServer({ + port, + ip: host, + render: () => , + htmlProps: { + scripts: [{path: '/extraScript.js'}], + styles: [{path: '/extraStyle.css'}], + headMarkup: , + }, + }), + ); + + const response = await wrapper.fetch('/'); + + await expect(response).toHaveBodyText( + ``, + ); + }); + it('supports updatable meta components', async () => { const myTitle = 'Shopify Mock App'; From d07550fbc78133b6054517546bf19162bce7e597 Mon Sep 17 00:00:00 2001 From: Ben Scott <227292+BPScott@users.noreply.github.com> Date: Fri, 23 Oct 2020 15:44:45 -0700 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Kaelig Deloumeau-Prigent --- packages/react-server/CHANGELOG.md | 2 +- packages/react-server/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-server/CHANGELOG.md b/packages/react-server/CHANGELOG.md index a88c07de2e..cf8697233b 100644 --- a/packages/react-server/CHANGELOG.md +++ b/packages/react-server/CHANGELOG.md @@ -7,7 +7,7 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -- Add `htmlProps` to the options for `createRender` and `createServer`, these props will be passed into the call to `@shopify/react-html`'s `` component ([#1661](https://github.com/Shopify/quilt/pull/1661)) +- Added `htmlProps` to the options for `createRender` and `createServer`, these props will be passed into the call to `@shopify/react-html`'s `` component ([#1661](https://github.com/Shopify/quilt/pull/1661)) ## [0.18.4] - 2020-10-20 diff --git a/packages/react-server/README.md b/packages/react-server/README.md index 0423e09ed4..3bdb4da6ed 100644 --- a/packages/react-server/README.md +++ b/packages/react-server/README.md @@ -87,7 +87,7 @@ interface Options { debug?: boolean; // a function similar to the render option but specifically used to render error pages for production SSR errors renderError: RenderFunction; - // additional props to pass into the Html component, or a function that takes a Koa.Context and returns a props object. + // additional props to pass into the Html component, or a function that takes a Koa.Context and returns a props object // See https://github.com/Shopify/quilt/blob/master/packages/react-html/README.md#html- htmlProps?: HtmlProps | (ctx: Context) => HtmlProps }