UNPKG

11.6 kBJavaScriptView Raw
1import prefetchHelper from "./prefetch"
2import emitter from "./emitter"
3import { setMatchPaths, findPath, findMatchPath } from "./find-path"
4
5const preferDefault = m => (m && m.default) || m
6
7const stripSurroundingSlashes = s => {
8 s = s[0] === `/` ? s.slice(1) : s
9 s = s.endsWith(`/`) ? s.slice(0, -1) : s
10 return s
11}
12
13const createPageDataUrl = path => {
14 const fixedPath = path === `/` ? `index` : stripSurroundingSlashes(path)
15 return `${__PATH_PREFIX__}/page-data/${fixedPath}/page-data.json`
16}
17
18const doFetch = (url, method = `GET`) =>
19 new Promise((resolve, reject) => {
20 const req = new XMLHttpRequest()
21 req.open(method, url, true)
22 req.onreadystatechange = () => {
23 if (req.readyState == 4) {
24 resolve(req)
25 }
26 }
27 req.send(null)
28 })
29
30const loadPageDataJson = loadObj => {
31 const { pagePath, retries = 0 } = loadObj
32 const url = createPageDataUrl(pagePath)
33 return doFetch(url).then(req => {
34 const { status, responseText } = req
35
36 // Handle 200
37 if (status === 200) {
38 try {
39 const jsonPayload = JSON.parse(responseText)
40 if (jsonPayload.path === undefined) {
41 throw new Error(`not a valid pageData response`)
42 }
43
44 return Object.assign(loadObj, {
45 status: `success`,
46 payload: jsonPayload,
47 })
48 } catch (err) {
49 // continue regardless of error
50 }
51 }
52
53 // Handle 404
54 if (status === 404 || status === 200) {
55 // If the request was for a 404 page and it doesn't exist, we're done
56 if (pagePath === `/404.html`) {
57 return Object.assign(loadObj, {
58 status: `failure`,
59 })
60 }
61
62 // Need some code here to cache the 404 request. In case
63 // multiple loadPageDataJsons result in 404s
64 return loadPageDataJson(
65 Object.assign(loadObj, { pagePath: `/404.html`, notFound: true })
66 )
67 }
68
69 // handle 500 response (Unrecoverable)
70 if (status === 500) {
71 return Object.assign(loadObj, {
72 status: `error`,
73 })
74 }
75
76 // Handle everything else, including status === 0, and 503s. Should retry
77 if (retries < 3) {
78 return loadPageDataJson(Object.assign(loadObj, { retries: retries + 1 }))
79 }
80
81 // Retried 3 times already, result is a failure.
82 return Object.assign(loadObj, {
83 status: `error`,
84 })
85 })
86}
87
88const doesConnectionSupportPrefetch = () => {
89 if (
90 `connection` in navigator &&
91 typeof navigator.connection !== `undefined`
92 ) {
93 if ((navigator.connection.effectiveType || ``).includes(`2g`)) {
94 return false
95 }
96 if (navigator.connection.saveData) {
97 return false
98 }
99 }
100 return true
101}
102
103const toPageResources = (pageData, component = null) => {
104 const page = {
105 componentChunkName: pageData.componentChunkName,
106 path: pageData.path,
107 webpackCompilationHash: pageData.webpackCompilationHash,
108 matchPath: pageData.matchPath,
109 }
110
111 return {
112 component,
113 json: pageData.result,
114 page,
115 }
116}
117
118export class BaseLoader {
119 constructor(loadComponent, matchPaths) {
120 // Map of pagePath -> Page. Where Page is an object with: {
121 // status: `success` || `error`,
122 // payload: PageResources, // undefined if `error`
123 // }
124 // PageResources is {
125 // component,
126 // json: pageData.result,
127 // page: {
128 // componentChunkName,
129 // path,
130 // webpackCompilationHash,
131 // }
132 // }
133 this.pageDb = new Map()
134 this.inFlightDb = new Map()
135 this.pageDataDb = new Map()
136 this.prefetchTriggered = new Set()
137 this.prefetchCompleted = new Set()
138 this.loadComponent = loadComponent
139 setMatchPaths(matchPaths)
140 }
141
142 setApiRunner(apiRunner) {
143 this.apiRunner = apiRunner
144 this.prefetchDisabled = apiRunner(`disableCorePrefetching`).some(a => a)
145 }
146
147 loadPageDataJson(rawPath) {
148 const pagePath = findPath(rawPath)
149 if (this.pageDataDb.has(pagePath)) {
150 return Promise.resolve(this.pageDataDb.get(pagePath))
151 }
152
153 return loadPageDataJson({ pagePath }).then(pageData => {
154 this.pageDataDb.set(pagePath, pageData)
155
156 return pageData
157 })
158 }
159
160 findMatchPath(rawPath) {
161 return findMatchPath(rawPath)
162 }
163
164 // TODO check all uses of this and whether they use undefined for page resources not exist
165 loadPage(rawPath) {
166 const pagePath = findPath(rawPath)
167 if (this.pageDb.has(pagePath)) {
168 const page = this.pageDb.get(pagePath)
169 return Promise.resolve(page.payload)
170 }
171 if (this.inFlightDb.has(pagePath)) {
172 return this.inFlightDb.get(pagePath)
173 }
174
175 const inFlight = Promise.all([
176 this.loadAppData(),
177 this.loadPageDataJson(pagePath),
178 ])
179 .then(allData => {
180 const result = allData[1]
181 if (result.status === `error`) {
182 return {
183 status: `error`,
184 }
185 }
186 if (result.status === `failure`) {
187 // throw an error so error trackers can pick this up
188 throw new Error(
189 `404 page could not be found. Checkout https://www.gatsbyjs.org/docs/add-404-page/`
190 )
191 }
192
193 let pageData = result.payload
194 const { componentChunkName } = pageData
195 return this.loadComponent(componentChunkName).then(component => {
196 const finalResult = { createdAt: new Date() }
197 let pageResources
198 if (!component) {
199 finalResult.status = `error`
200 } else {
201 finalResult.status = `success`
202 if (result.notFound === true) {
203 finalResult.notFound = true
204 }
205 pageData = Object.assign(pageData, {
206 webpackCompilationHash: allData[0]
207 ? allData[0].webpackCompilationHash
208 : ``,
209 })
210 pageResources = toPageResources(pageData, component)
211 finalResult.payload = pageResources
212 emitter.emit(`onPostLoadPageResources`, {
213 page: pageResources,
214 pageResources,
215 })
216 }
217 this.pageDb.set(pagePath, finalResult)
218 // undefined if final result is an error
219 return pageResources
220 })
221 })
222 // prefer duplication with then + catch over .finally to prevent problems in ie11 + firefox
223 .then(response => {
224 this.inFlightDb.delete(pagePath)
225 return response
226 })
227 .catch(err => {
228 this.inFlightDb.delete(pagePath)
229 throw err
230 })
231
232 this.inFlightDb.set(pagePath, inFlight)
233 return inFlight
234 }
235
236 // returns undefined if loading page ran into errors
237 loadPageSync(rawPath) {
238 const pagePath = findPath(rawPath)
239 if (this.pageDb.has(pagePath)) {
240 return this.pageDb.get(pagePath).payload
241 }
242 return undefined
243 }
244
245 shouldPrefetch(pagePath) {
246 // Skip prefetching if we know user is on slow or constrained connection
247 if (!doesConnectionSupportPrefetch()) {
248 return false
249 }
250
251 // Check if the page exists.
252 if (this.pageDb.has(pagePath)) {
253 return false
254 }
255
256 return true
257 }
258
259 prefetch(pagePath) {
260 if (!this.shouldPrefetch(pagePath)) {
261 return false
262 }
263
264 // Tell plugins with custom prefetching logic that they should start
265 // prefetching this path.
266 if (!this.prefetchTriggered.has(pagePath)) {
267 this.apiRunner(`onPrefetchPathname`, { pathname: pagePath })
268 this.prefetchTriggered.add(pagePath)
269 }
270
271 // If a plugin has disabled core prefetching, stop now.
272 if (this.prefetchDisabled) {
273 return false
274 }
275
276 const realPath = findPath(pagePath)
277 // Todo make doPrefetch logic cacheable
278 // eslint-disable-next-line consistent-return
279 this.doPrefetch(realPath).then(() => {
280 if (!this.prefetchCompleted.has(pagePath)) {
281 this.apiRunner(`onPostPrefetchPathname`, { pathname: pagePath })
282 this.prefetchCompleted.add(pagePath)
283 }
284 })
285
286 return true
287 }
288
289 doPrefetch(pagePath) {
290 throw new Error(`doPrefetch not implemented`)
291 }
292
293 hovering(rawPath) {
294 this.loadPage(rawPath)
295 }
296
297 getResourceURLsForPathname(rawPath) {
298 const pagePath = findPath(rawPath)
299 const page = this.pageDataDb.get(pagePath)
300 if (page) {
301 const pageResources = toPageResources(page.payload)
302
303 return [
304 ...createComponentUrls(pageResources.page.componentChunkName),
305 createPageDataUrl(pagePath),
306 ]
307 } else {
308 return null
309 }
310 }
311
312 isPageNotFound(rawPath) {
313 const pagePath = findPath(rawPath)
314 const page = this.pageDb.get(pagePath)
315 return page && page.notFound === true
316 }
317
318 loadAppData(retries = 0) {
319 return doFetch(`${__PATH_PREFIX__}/page-data/app-data.json`).then(req => {
320 const { status, responseText } = req
321
322 let appData
323
324 if (status !== 200 && retries < 3) {
325 // Retry 3 times incase of failures
326 return this.loadAppData(retries + 1)
327 }
328
329 // Handle 200
330 if (status === 200) {
331 try {
332 const jsonPayload = JSON.parse(responseText)
333 if (jsonPayload.webpackCompilationHash === undefined) {
334 throw new Error(`not a valid app-data response`)
335 }
336
337 appData = jsonPayload
338 } catch (err) {
339 // continue regardless of error
340 }
341 }
342
343 return appData
344 })
345 }
346}
347
348const createComponentUrls = componentChunkName =>
349 window.___chunkMapping[componentChunkName].map(
350 chunk => __PATH_PREFIX__ + chunk
351 )
352
353export class ProdLoader extends BaseLoader {
354 constructor(asyncRequires, matchPaths) {
355 const loadComponent = chunkName =>
356 asyncRequires.components[chunkName]().then(preferDefault)
357
358 super(loadComponent, matchPaths)
359 }
360
361 doPrefetch(pagePath) {
362 const pageDataUrl = createPageDataUrl(pagePath)
363 return prefetchHelper(pageDataUrl, {
364 crossOrigin: `anonymous`,
365 as: `fetch`,
366 })
367 .then(() =>
368 // This was just prefetched, so will return a response from
369 // the cache instead of making another request to the server
370 this.loadPageDataJson(pagePath)
371 )
372 .then(result => {
373 if (result.status !== `success`) {
374 return Promise.resolve()
375 }
376 const pageData = result.payload
377 const chunkName = pageData.componentChunkName
378 const componentUrls = createComponentUrls(chunkName)
379 return Promise.all(componentUrls.map(prefetchHelper)).then(
380 () => pageData
381 )
382 })
383 }
384}
385
386let instance
387
388export const setLoader = _loader => {
389 instance = _loader
390}
391
392export const publicLoader = {
393 // Deprecated methods. As far as we're aware, these are only used by
394 // core gatsby and the offline plugin, however there's a very small
395 // chance they're called by others.
396 getResourcesForPathname: rawPath => {
397 console.warn(
398 `Warning: getResourcesForPathname is deprecated. Use loadPage instead`
399 )
400 return instance.i.loadPage(rawPath)
401 },
402 getResourcesForPathnameSync: rawPath => {
403 console.warn(
404 `Warning: getResourcesForPathnameSync is deprecated. Use loadPageSync instead`
405 )
406 return instance.i.loadPageSync(rawPath)
407 },
408 enqueue: rawPath => instance.prefetch(rawPath),
409
410 // Real methods
411 getResourceURLsForPathname: rawPath =>
412 instance.getResourceURLsForPathname(rawPath),
413 loadPage: rawPath => instance.loadPage(rawPath),
414 loadPageSync: rawPath => instance.loadPageSync(rawPath),
415 prefetch: rawPath => instance.prefetch(rawPath),
416 isPageNotFound: rawPath => instance.isPageNotFound(rawPath),
417 hovering: rawPath => instance.hovering(rawPath),
418 loadAppData: () => instance.loadAppData(),
419}
420
421export default publicLoader