UNPKG

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