Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
@skava/react-server / src / serverSideRender / HTML / ServerHTML.tsx
Size: Mime:
/**
 * @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