1 | const React = require(`react`)
|
2 | const fs = require(`fs`)
|
3 | const { join } = require(`path`)
|
4 | const { renderToString, renderToStaticMarkup } = require(`react-dom/server`)
|
5 | const { ServerLocation, Router, isRedirect } = require(`@reach/router`)
|
6 | const {
|
7 | get,
|
8 | merge,
|
9 | isObject,
|
10 | flatten,
|
11 | uniqBy,
|
12 | flattenDeep,
|
13 | replace,
|
14 | concat,
|
15 | } = require(`lodash`)
|
16 |
|
17 | const apiRunner = require(`./api-runner-ssr`)
|
18 | const syncRequires = require(`./sync-requires`)
|
19 | const { version: gatsbyVersion } = require(`gatsby/package.json`)
|
20 |
|
21 | const stats = JSON.parse(
|
22 | fs.readFileSync(`${process.cwd()}/public/webpack.stats.json`, `utf-8`)
|
23 | )
|
24 |
|
25 | const chunkMapping = JSON.parse(
|
26 | fs.readFileSync(`${process.cwd()}/public/chunk-map.json`, `utf-8`)
|
27 | )
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | const testRequireError = (moduleName, err) => {
|
34 | const regex = new RegExp(`Error: Cannot find module\\s.${moduleName}`)
|
35 | const firstLine = err.toString().split(`\n`)[0]
|
36 | return regex.test(firstLine)
|
37 | }
|
38 |
|
39 | let Html
|
40 | try {
|
41 | Html = require(`../src/html`)
|
42 | } catch (err) {
|
43 | if (testRequireError(`../src/html`, err)) {
|
44 | Html = require(`./default-html`)
|
45 | } else {
|
46 | throw err
|
47 | }
|
48 | }
|
49 |
|
50 | Html = Html && Html.__esModule ? Html.default : Html
|
51 |
|
52 | const getPageDataPath = path => {
|
53 | const fixedPagePath = path === `/` ? `index` : path
|
54 | return join(`page-data`, fixedPagePath, `page-data.json`)
|
55 | }
|
56 |
|
57 | const getPageDataUrl = pagePath => {
|
58 | const pageDataPath = getPageDataPath(pagePath)
|
59 | return `${__PATH_PREFIX__}/${pageDataPath}`
|
60 | }
|
61 |
|
62 | const getPageDataFile = pagePath => {
|
63 | const pageDataPath = getPageDataPath(pagePath)
|
64 | return join(process.cwd(), `public`, pageDataPath)
|
65 | }
|
66 |
|
67 | const loadPageDataSync = pagePath => {
|
68 | const pageDataPath = getPageDataPath(pagePath)
|
69 | const pageDataFile = join(process.cwd(), `public`, pageDataPath)
|
70 | try {
|
71 | const pageDataJson = fs.readFileSync(pageDataFile)
|
72 | return JSON.parse(pageDataJson)
|
73 | } catch (error) {
|
74 |
|
75 | return null
|
76 | }
|
77 | }
|
78 |
|
79 | const createElement = React.createElement
|
80 |
|
81 | export const sanitizeComponents = components => {
|
82 | const componentsArray = ensureArray(components)
|
83 | return componentsArray.map(component => {
|
84 |
|
85 |
|
86 | if (__ASSET_PREFIX__ && component.props.rel === `manifest`) {
|
87 | return React.cloneElement(component, {
|
88 | href: replace(component.props.href, __ASSET_PREFIX__, ``),
|
89 | })
|
90 | }
|
91 | return component
|
92 | })
|
93 | }
|
94 |
|
95 | const ensureArray = components => {
|
96 | if (Array.isArray(components)) {
|
97 |
|
98 | return flattenDeep(
|
99 | components.filter(val => (Array.isArray(val) ? val.length > 0 : val))
|
100 | )
|
101 | } else {
|
102 |
|
103 | return components ? [components] : []
|
104 | }
|
105 | }
|
106 |
|
107 | export default (pagePath, callback) => {
|
108 | let bodyHtml = ``
|
109 | let headComponents = [
|
110 | <meta
|
111 | name="generator"
|
112 | content={`Gatsby ${gatsbyVersion}`}
|
113 | key={`generator-${gatsbyVersion}`}
|
114 | />,
|
115 | ]
|
116 | let htmlAttributes = {}
|
117 | let bodyAttributes = {}
|
118 | let preBodyComponents = []
|
119 | let postBodyComponents = []
|
120 | let bodyProps = {}
|
121 |
|
122 | const replaceBodyHTMLString = body => {
|
123 | bodyHtml = body
|
124 | }
|
125 |
|
126 | const setHeadComponents = components => {
|
127 | headComponents = headComponents.concat(sanitizeComponents(components))
|
128 | }
|
129 |
|
130 | const setHtmlAttributes = attributes => {
|
131 | htmlAttributes = merge(htmlAttributes, attributes)
|
132 | }
|
133 |
|
134 | const setBodyAttributes = attributes => {
|
135 | bodyAttributes = merge(bodyAttributes, attributes)
|
136 | }
|
137 |
|
138 | const setPreBodyComponents = components => {
|
139 | preBodyComponents = preBodyComponents.concat(sanitizeComponents(components))
|
140 | }
|
141 |
|
142 | const setPostBodyComponents = components => {
|
143 | postBodyComponents = postBodyComponents.concat(
|
144 | sanitizeComponents(components)
|
145 | )
|
146 | }
|
147 |
|
148 | const setBodyProps = props => {
|
149 | bodyProps = merge({}, bodyProps, props)
|
150 | }
|
151 |
|
152 | const getHeadComponents = () => headComponents
|
153 |
|
154 | const replaceHeadComponents = components => {
|
155 | headComponents = sanitizeComponents(components)
|
156 | }
|
157 |
|
158 | const getPreBodyComponents = () => preBodyComponents
|
159 |
|
160 | const replacePreBodyComponents = components => {
|
161 | preBodyComponents = sanitizeComponents(components)
|
162 | }
|
163 |
|
164 | const getPostBodyComponents = () => postBodyComponents
|
165 |
|
166 | const replacePostBodyComponents = components => {
|
167 | postBodyComponents = sanitizeComponents(components)
|
168 | }
|
169 |
|
170 | const pageDataRaw = fs.readFileSync(getPageDataFile(pagePath))
|
171 | const pageData = JSON.parse(pageDataRaw)
|
172 | const pageDataUrl = getPageDataUrl(pagePath)
|
173 | const { componentChunkName } = pageData
|
174 |
|
175 | class RouteHandler extends React.Component {
|
176 | render() {
|
177 | const props = {
|
178 | ...this.props,
|
179 | ...pageData.result,
|
180 |
|
181 | pathContext: pageData.result ? pageData.result.pageContext : undefined,
|
182 | }
|
183 |
|
184 | const pageElement = createElement(
|
185 | syncRequires.components[componentChunkName],
|
186 | props
|
187 | )
|
188 |
|
189 | const wrappedPage = apiRunner(
|
190 | `wrapPageElement`,
|
191 | { element: pageElement, props },
|
192 | pageElement,
|
193 | ({ result }) => {
|
194 | return { element: result, props }
|
195 | }
|
196 | ).pop()
|
197 |
|
198 | return wrappedPage
|
199 | }
|
200 | }
|
201 |
|
202 | const routerElement = createElement(
|
203 | ServerLocation,
|
204 | { url: `${__BASE_PATH__}${pagePath}` },
|
205 | createElement(
|
206 | Router,
|
207 | {
|
208 | id: `gatsby-focus-wrapper`,
|
209 | baseuri: `${__BASE_PATH__}`,
|
210 | },
|
211 | createElement(RouteHandler, { path: `/*` })
|
212 | )
|
213 | )
|
214 |
|
215 | const bodyComponent = apiRunner(
|
216 | `wrapRootElement`,
|
217 | { element: routerElement, pathname: pagePath },
|
218 | routerElement,
|
219 | ({ result }) => {
|
220 | return { element: result, pathname: pagePath }
|
221 | }
|
222 | ).pop()
|
223 |
|
224 |
|
225 | apiRunner(`replaceRenderer`, {
|
226 | bodyComponent,
|
227 | replaceBodyHTMLString,
|
228 | setHeadComponents,
|
229 | setHtmlAttributes,
|
230 | setBodyAttributes,
|
231 | setPreBodyComponents,
|
232 | setPostBodyComponents,
|
233 | setBodyProps,
|
234 | pathname: pagePath,
|
235 | pathPrefix: __PATH_PREFIX__,
|
236 | })
|
237 |
|
238 |
|
239 | if (!bodyHtml) {
|
240 | try {
|
241 | bodyHtml = renderToString(bodyComponent)
|
242 | } catch (e) {
|
243 |
|
244 | if (!isRedirect(e)) throw e
|
245 | }
|
246 | }
|
247 |
|
248 |
|
249 | let scriptsAndStyles = flatten(
|
250 | [`app`, componentChunkName].map(s => {
|
251 | const fetchKey = `assetsByChunkName[${s}]`
|
252 |
|
253 | let chunks = get(stats, fetchKey)
|
254 | let namedChunkGroups = get(stats, `namedChunkGroups`)
|
255 |
|
256 | if (!chunks) {
|
257 | return null
|
258 | }
|
259 |
|
260 | chunks = chunks.map(chunk => {
|
261 | if (chunk === `/`) {
|
262 | return null
|
263 | }
|
264 | return { rel: `preload`, name: chunk }
|
265 | })
|
266 |
|
267 | namedChunkGroups[s].assets.forEach(asset =>
|
268 | chunks.push({ rel: `preload`, name: asset })
|
269 | )
|
270 |
|
271 | const childAssets = namedChunkGroups[s].childAssets
|
272 | for (const rel in childAssets) {
|
273 | chunks = concat(
|
274 | chunks,
|
275 | childAssets[rel].map(chunk => {
|
276 | return { rel, name: chunk }
|
277 | })
|
278 | )
|
279 | }
|
280 |
|
281 | return chunks
|
282 | })
|
283 | )
|
284 | .filter(s => isObject(s))
|
285 | .sort((s1, s2) => (s1.rel == `preload` ? -1 : 1))
|
286 |
|
287 | scriptsAndStyles = uniqBy(scriptsAndStyles, item => item.name)
|
288 |
|
289 | const scripts = scriptsAndStyles.filter(
|
290 | script => script.name && script.name.endsWith(`.js`)
|
291 | )
|
292 | const styles = scriptsAndStyles.filter(
|
293 | style => style.name && style.name.endsWith(`.css`)
|
294 | )
|
295 |
|
296 | apiRunner(`onRenderBody`, {
|
297 | setHeadComponents,
|
298 | setHtmlAttributes,
|
299 | setBodyAttributes,
|
300 | setPreBodyComponents,
|
301 | setPostBodyComponents,
|
302 | setBodyProps,
|
303 | pathname: pagePath,
|
304 | loadPageDataSync,
|
305 | bodyHtml,
|
306 | scripts,
|
307 | styles,
|
308 | pathPrefix: __PATH_PREFIX__,
|
309 | })
|
310 |
|
311 | scripts
|
312 | .slice(0)
|
313 | .reverse()
|
314 | .forEach(script => {
|
315 |
|
316 | headComponents.push(
|
317 | <link
|
318 | as="script"
|
319 | rel={script.rel}
|
320 | key={script.name}
|
321 | href={`${__PATH_PREFIX__}/${script.name}`}
|
322 | />
|
323 | )
|
324 | })
|
325 |
|
326 | if (pageData) {
|
327 | headComponents.push(
|
328 | <link
|
329 | as="fetch"
|
330 | rel="preload"
|
331 | key={pageDataUrl}
|
332 | href={pageDataUrl}
|
333 | crossOrigin="anonymous"
|
334 | />
|
335 | )
|
336 | }
|
337 |
|
338 | styles
|
339 | .slice(0)
|
340 | .reverse()
|
341 | .forEach(style => {
|
342 |
|
343 |
|
344 |
|
345 | if (style.rel === `prefetch`) {
|
346 | headComponents.push(
|
347 | <link
|
348 | as="style"
|
349 | rel={style.rel}
|
350 | key={style.name}
|
351 | href={`${__PATH_PREFIX__}/${style.name}`}
|
352 | />
|
353 | )
|
354 | } else {
|
355 | headComponents.unshift(
|
356 | <style
|
357 | data-href={`${__PATH_PREFIX__}/${style.name}`}
|
358 | dangerouslySetInnerHTML={{
|
359 | __html: fs.readFileSync(
|
360 | join(process.cwd(), `public`, style.name),
|
361 | `utf-8`
|
362 | ),
|
363 | }}
|
364 | />
|
365 | )
|
366 | }
|
367 | })
|
368 |
|
369 |
|
370 | const windowPageData = `/*<![CDATA[*/window.pagePath="${pagePath}";/*]]>*/`
|
371 |
|
372 | postBodyComponents.push(
|
373 | <script
|
374 | key={`script-loader`}
|
375 | id={`gatsby-script-loader`}
|
376 | dangerouslySetInnerHTML={{
|
377 | __html: windowPageData,
|
378 | }}
|
379 | />
|
380 | )
|
381 |
|
382 |
|
383 | const scriptChunkMapping = `/*<![CDATA[*/window.___chunkMapping=${JSON.stringify(
|
384 | chunkMapping
|
385 | )};/*]]>*/`
|
386 |
|
387 | postBodyComponents.push(
|
388 | <script
|
389 | key={`chunk-mapping`}
|
390 | id={`gatsby-chunk-mapping`}
|
391 | dangerouslySetInnerHTML={{
|
392 | __html: scriptChunkMapping,
|
393 | }}
|
394 | />
|
395 | )
|
396 |
|
397 |
|
398 |
|
399 | const bodyScripts = scripts
|
400 | .filter(s => s.rel !== `prefetch`)
|
401 | .map(s => {
|
402 | const scriptPath = `${__PATH_PREFIX__}/${JSON.stringify(s.name).slice(
|
403 | 1,
|
404 | -1
|
405 | )}`
|
406 | return <script key={scriptPath} src={scriptPath} async />
|
407 | })
|
408 |
|
409 | postBodyComponents.push(...bodyScripts)
|
410 |
|
411 | apiRunner(`onPreRenderHTML`, {
|
412 | getHeadComponents,
|
413 | replaceHeadComponents,
|
414 | getPreBodyComponents,
|
415 | replacePreBodyComponents,
|
416 | getPostBodyComponents,
|
417 | replacePostBodyComponents,
|
418 | pathname: pagePath,
|
419 | pathPrefix: __PATH_PREFIX__,
|
420 | })
|
421 |
|
422 | const html = `<!DOCTYPE html>${renderToStaticMarkup(
|
423 | <Html
|
424 | {...bodyProps}
|
425 | headComponents={headComponents}
|
426 | htmlAttributes={htmlAttributes}
|
427 | bodyAttributes={bodyAttributes}
|
428 | preBodyComponents={preBodyComponents}
|
429 | postBodyComponents={postBodyComponents}
|
430 | body={bodyHtml}
|
431 | path={pagePath}
|
432 | />
|
433 | )}`
|
434 |
|
435 | callback(null, html)
|
436 | }
|