UNPKG

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