Repository URL to install this package:
|
Version:
1.2.7 ▾
|
/**
* @module responsible for generating the HTML page response
* @see the react application middleware
*/
/* eslint-disable react/no-danger */
/* eslint-disable react/no-array-index-key */
import React from 'react'
import { ComponentType } from 'react'
import serialize from 'serialize-javascript'
import { getClientBundleEntryAssets } from '@skava/bs'
import { HTML } from '../Document'
import {
ServerHtmlOtherProps,
ServerHTMLProps,
GlobalScriptTags,
InlineScriptProps,
KeyedComponentProps,
} from './typings'
const clientEntryAssets = getClientBundleEntryAssets()
// --------------- config ---------------
const HAS_STYLES = false
// ----------------- components ------------------
/**
* make sure we don't give any `map` files
* @todo we could filter this elsewhere
* @param path @example /eh.js
*/
function shouldRemoveFromServerJavaScriptList(path: string) {
return path.includes('.map') === false
}
/**
* @throws Invariant Violation: React.Children.only expected to receive a single React element child.
* return React.Children.only(props.children)
*/
function KeyedComponent(props: KeyedComponentProps) {
return props.children
}
const toKeyed = (x: ComponentType, index: string | number) => (
<KeyedComponent key={index}>{x}</KeyedComponent>
)
function stylesheetTag(stylesheetFilePath: string) {
return (
<link
href={stylesheetFilePath}
media="screen, projection"
rel="stylesheet"
// https://github.com/styled-components/styled-components/releases/tag/v3.2.1
// type="text/css"
/>
)
}
function scriptTag(jsFilePath: string) {
const finalPath = jsFilePath.includes('/') ? jsFilePath : '/' + jsFilePath
return <script type="text/javascript" src={finalPath} />
}
/**
* @example <script>{stringProtectedByNonce}</script>
* @tutorial https://stackoverflow.com/questions/42922784/what-s-the-purpose-of-the-html-nonce-attribute-for-script-and-style-elements
*/
const inlineScript = (props: InlineScriptProps) => (
<script
nonce={props.nonce}
type="text/javascript"
dangerouslySetInnerHTML={{ __html: props.children }}
/>
)
/**
* this renders the elements wrapped with <HelmetReact> on our views/pages
*/
function createHeaderElements(props: ServerHtmlOtherProps) {
const { helmet, styledTags } = props
const noHelmet = !helmet
let styleElements = []
if (HAS_STYLES && noHelmet) {
const styleTag = stylesheetTag(clientEntryAssets.css)
styleElements.push(styleTag)
} else if (helmet) {
styleElements = helmet.style.toComponent() || []
}
if (styledTags) {
styleElements.push(styledTags)
}
return noHelmet
? []
: [
...helmet.title.toComponent(),
...helmet.base.toComponent(),
...helmet.meta.toComponent(),
...helmet.link.toComponent(),
...helmet.style.toComponent(),
...styleElements,
]
}
// ----------------- core ------------------
/**
* @todo see view-container and the byte minification there
*
* @example if (storeState)
* inlineScriptString += `window.__APP_STATE__`
* inlineScriptString += `=` + serialize(storeState);
*
* @return {String} JSON of the serialized state
*/
function fromGlobalStateToScriptString(
inlineScriptState: GlobalScriptTags<Object>
) {
let inlineScriptString = ``
const GLOBAL_NAMES = Object.keys(inlineScriptState)
for (let index = 0; index < GLOBAL_NAMES.length; index++) {
const GLOBAL_NAME = GLOBAL_NAMES[index]
const GLOBAL_VALUE = inlineScriptState[GLOBAL_NAME]
inlineScriptString += `window.${GLOBAL_NAME} = ${serialize(
GLOBAL_VALUE
)};\n`
}
return inlineScriptString
}
/**
* for rehydrating state from a <script> added to the end of <body>
* full of JSON dumps
*/
function getRehydratableScript(props: ServerHTMLProps) {
const { nonce } = props
const { routerState, asyncState, storeState, apolloState } = props
const inlineScriptState: GlobalScriptTags<Object> = {
__APP_STATE__: storeState,
__APOLLO_STATE__: apolloState,
__ROUTER_STATE__: routerState,
__ASYNC_COMPONENTS_STATE__: asyncState,
}
const children = fromGlobalStateToScriptString(inlineScriptState)
return inlineScript({ children, nonce })
}
function getBodyElements(props: ServerHTMLProps) {
const { helmet } = props
const bodyElements = []
const rehydratable = getRehydratableScript(props)
bodyElements.push(rehydratable)
// @todo @@perf use exotic
if (clientEntryAssets && clientEntryAssets.jsList) {
clientEntryAssets.jsList
.filter(shouldRemoveFromServerJavaScriptList)
.map(scriptTag)
.forEach(asScriptTag => {
bodyElements.push(asScriptTag)
})
}
// @todo @@perf use exotic
// every page with a <Helmet> has this
// should just require it to simplify
if (helmet) {
// currently this never happens
helmet.script
.toComponent()
.forEach(scriptComponent => bodyElements.push(scriptComponent))
}
return bodyElements
}
class ServerHTML extends React.PureComponent<ServerHTMLProps> {
render() {
const { helmet, reactAppString } = this.props
const headerElements = createHeaderElements(this.props)
const attributes = helmet ? helmet.htmlAttributes.toComponent() : undefined
const bodyElements = getBodyElements(this.props)
/* eslint-disable react/jsx-pascal-case */
return (
<HTML
htmlAttributes={attributes}
headerElements={headerElements.map(toKeyed)}
bodyElements={bodyElements.map(toKeyed)}
appBodyString={reactAppString}
/>
)
}
}
export { ServerHTML }
export default ServerHTML