UNPKG

11.1 kBJavaScriptView Raw
1const React = require(`react`)
2const fs = require(`fs`)
3const { join } = require(`path`)
4const { renderToString, renderToStaticMarkup } = require(`react-dom/server`)
5const { ServerLocation, Router, isRedirect } = require(`@reach/router`)
6const {
7 get,
8 merge,
9 isObject,
10 flatten,
11 uniqBy,
12 flattenDeep,
13 replace,
14 concat,
15} = require(`lodash`)
16
17const apiRunner = require(`./api-runner-ssr`)
18const syncRequires = require(`./sync-requires`)
19const { version: gatsbyVersion } = require(`gatsby/package.json`)
20
21const stats = JSON.parse(
22 fs.readFileSync(`${process.cwd()}/public/webpack.stats.json`, `utf-8`)
23)
24
25const chunkMapping = JSON.parse(
26 fs.readFileSync(`${process.cwd()}/public/chunk-map.json`, `utf-8`)
27)
28
29// const testRequireError = require("./test-require-error")
30// For some extremely mysterious reason, webpack adds the above module *after*
31// this module so that when this code runs, testRequireError is undefined.
32// So in the meantime, we'll just inline it.
33const 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
39let Html
40try {
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
50Html = Html && Html.__esModule ? Html.default : Html
51
52const getPageDataPath = path => {
53 const fixedPagePath = path === `/` ? `index` : path
54 return join(`page-data`, fixedPagePath, `page-data.json`)
55}
56
57const getPageDataUrl = pagePath => {
58 const pageDataPath = getPageDataPath(pagePath)
59 return `${__PATH_PREFIX__}/${pageDataPath}`
60}
61
62const getPageDataFile = pagePath => {
63 const pageDataPath = getPageDataPath(pagePath)
64 return join(process.cwd(), `public`, pageDataPath)
65}
66
67const 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 // not an error if file is not found. There's just no page data
75 return null
76 }
77}
78
79const createElement = React.createElement
80
81export const sanitizeComponents = components => {
82 const componentsArray = ensureArray(components)
83 return componentsArray.map(component => {
84 // Ensure manifest is always loaded from content server
85 // And not asset server when an assetPrefix is used
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
95const ensureArray = components => {
96 if (Array.isArray(components)) {
97 // remove falsy items and flatten
98 return flattenDeep(
99 components.filter(val => (Array.isArray(val) ? val.length > 0 : val))
100 )
101 } else {
102 // we also accept single components, so we need to handle this case as well
103 return components ? [components] : []
104 }
105}
106
107export 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 // pathContext was deprecated in v2. Renamed to pageContext
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 // Let the site or plugin render the page component.
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 // If no one stepped up, we'll handle it.
239 if (!bodyHtml) {
240 try {
241 bodyHtml = renderToString(bodyComponent)
242 } catch (e) {
243 // ignore @reach/router redirect errors
244 if (!isRedirect(e)) throw e
245 }
246 }
247
248 // Create paths to scripts
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)) // given priority to preload
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 // Add preload/prefetch <link>s for scripts.
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 // Add <link>s for styles that should be prefetched
343 // otherwise, inline as a <style> tag
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 // Add page metadata for the current page
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 // Add chunk mapping metadata
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 // Filter out prefetched bundles as adding them as a script tag
398 // would force high priority fetching.
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}