UNPKG

cl-react-graph

Version:
420 lines (345 loc) 13.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports.default = exports.publicLoader = exports.setApiRunnerForLoader = exports.postInitialRenderWork = void 0; var _findPage = _interopRequireDefault(require("./find-page")); var _emitter = _interopRequireDefault(require("./emitter")); var _prefetch = _interopRequireDefault(require("./prefetch")); const preferDefault = m => m && m.default || m; let devGetPageData; let inInitialRender = true; let hasFetched = Object.create(null); let syncRequires = {}; let asyncRequires = {}; let jsonDataPaths = {}; let fetchHistory = []; let fetchingPageResourceMapPromise = null; let fetchedPageResourceMap = false; /** * Indicate if pages manifest is loaded * - in production it is split to separate "pages-manifest" chunk that need to be lazy loaded, * - in development it is part of single "common" chunk and is available from the start. */ let hasPageResourceMap = process.env.NODE_ENV !== `production`; let apiRunner; const failedPaths = {}; const MAX_HISTORY = 5; const jsonPromiseStore = {}; if (process.env.NODE_ENV !== `production`) { devGetPageData = require(`./socketIo`).getPageData; } /** * Fetch resource map (pages data and paths to json files with results of * queries) */ const fetchPageResourceMap = () => { if (!fetchingPageResourceMapPromise) { fetchingPageResourceMapPromise = new Promise(resolve => { asyncRequires.data().then(({ pages, dataPaths }) => { // TODO — expose proper way to access this data from plugins. // Need to come up with an API for plugins to access // site info. window.___dataPaths = dataPaths; queue.addPagesArray(pages); queue.addDataPaths(dataPaths); hasPageResourceMap = true; resolve(fetchedPageResourceMap = true); }).catch(e => { console.warn(`Failed to fetch pages manifest. Gatsby will reload on next navigation.`); // failed to grab pages metadata // for now let's just resolve this - on navigation this will cause missing resources // and will trigger page reload and then it will retry // this can happen with service worker updates when webpack manifest points to old // chunk that no longer exists on server resolve(fetchedPageResourceMap = true); }); }); } return fetchingPageResourceMapPromise; }; const createJsonURL = jsonName => `${__PATH_PREFIX__}/static/d/${jsonName}.json`; const createComponentUrls = componentChunkName => window.___chunkMapping[componentChunkName].map(chunk => __PATH_PREFIX__ + chunk); const fetchResource = resourceName => { // Find resource let resourceFunction; if (resourceName.slice(0, 12) === `component---`) { resourceFunction = asyncRequires.components[resourceName]; } else { if (resourceName in jsonPromiseStore) { resourceFunction = () => jsonPromiseStore[resourceName]; } else { resourceFunction = () => { const fetchPromise = new Promise((resolve, reject) => { const url = createJsonURL(jsonDataPaths[resourceName]); const req = new XMLHttpRequest(); req.open(`GET`, url, true); req.withCredentials = true; req.onreadystatechange = () => { if (req.readyState == 4) { if (req.status === 200) { resolve(JSON.parse(req.responseText)); } else { delete jsonPromiseStore[resourceName]; reject(); } } }; req.send(null); }); jsonPromiseStore[resourceName] = fetchPromise; return fetchPromise; }; } } // Download the resource hasFetched[resourceName] = true; return new Promise(resolve => { const fetchPromise = resourceFunction(); let failed = false; return fetchPromise.catch(() => { failed = true; }).then(component => { fetchHistory.push({ resource: resourceName, succeeded: !failed }); fetchHistory = fetchHistory.slice(-MAX_HISTORY); resolve(component); }); }); }; const prefetchResource = resourceName => { if (resourceName.slice(0, 12) === `component---`) { return Promise.all(createComponentUrls(resourceName).map(url => (0, _prefetch.default)(url))); } else { const url = createJsonURL(jsonDataPaths[resourceName]); return (0, _prefetch.default)(url); } }; const getResourceModule = resourceName => fetchResource(resourceName).then(preferDefault); const appearsOnLine = () => { const isOnLine = navigator.onLine; if (typeof isOnLine === `boolean`) { return isOnLine; } // If no navigator.onLine support assume onLine if any of last N fetches succeeded const succeededFetch = fetchHistory.find(entry => entry.succeeded); return !!succeededFetch; }; const handleResourceLoadError = (path, message) => { if (!failedPaths[path]) { failedPaths[path] = message; } if (appearsOnLine() && window.location.pathname.replace(/\/$/g, ``) !== path.replace(/\/$/g, ``)) { window.location.pathname = path; } }; const onPrefetchPathname = pathname => { if (!prefetchTriggered[pathname]) { apiRunner(`onPrefetchPathname`, { pathname }); prefetchTriggered[pathname] = true; } }; const onPostPrefetchPathname = pathname => { if (!prefetchCompleted[pathname]) { apiRunner(`onPostPrefetchPathname`, { pathname }); prefetchCompleted[pathname] = true; } }; /** * Check if we should fallback to resources for 404 page if resources for a page are not found * * We can't do that when we don't have full pages manifest - we don't know if page exist or not if we don't have it. * We also can't do that on initial render / mount in case we just can't load resources needed for first page. * Not falling back to 404 resources will cause "EnsureResources" component to handle scenarios like this with * potential reload * @param {string} path Path to a page */ const shouldFallbackTo404Resources = path => (hasPageResourceMap || inInitialRender) && path !== `/404.html`; // Note we're not actively using the path data atm. There // could be future optimizations however around trying to ensure // we load all resources for likely-to-be-visited paths. // let pathArray = [] // let pathCount = {} let findPage; let pathScriptsCache = {}; let prefetchTriggered = {}; let prefetchCompleted = {}; let disableCorePrefetching = false; const queue = { addPagesArray: newPages => { findPage = (0, _findPage.default)(newPages, __PATH_PREFIX__); }, addDevRequires: devRequires => { syncRequires = devRequires; }, addProdRequires: prodRequires => { asyncRequires = prodRequires; }, addDataPaths: dataPaths => { jsonDataPaths = dataPaths; }, // Hovering on a link is a very strong indication the user is going to // click on it soon so let's start prefetching resources for this // pathname. hovering: path => { queue.getResourcesForPathname(path); }, enqueue: path => { if (!apiRunner) console.error(`Run setApiRunnerForLoader() before enqueing paths`); // Skip prefetching if we know user is on slow or constrained connection if (`connection` in navigator) { if ((navigator.connection.effectiveType || ``).includes(`2g`)) { return false; } if (navigator.connection.saveData) { return false; } } // Tell plugins with custom prefetching logic that they should start // prefetching this path. onPrefetchPathname(path); // If a plugin has disabled core prefetching, stop now. if (disableCorePrefetching.some(a => a)) { return false; } // Check if the page exists. let page = findPage(path); // In production, we lazy load page metadata. If that // hasn't been fetched yet, start fetching it now. if (process.env.NODE_ENV === `production` && !page && !fetchedPageResourceMap) { // If page wasn't found check and we didn't fetch resources map for // all pages, wait for fetch to complete and try find page again return fetchPageResourceMap().then(() => queue.enqueue(path)); } if (!page) { return false; } if (process.env.NODE_ENV !== `production` && process.env.NODE_ENV !== `test`) { devGetPageData(page.path); } // Prefetch resources. if (process.env.NODE_ENV === `production`) { Promise.all([prefetchResource(page.jsonName), prefetchResource(page.componentChunkName)]).then(() => { // Tell plugins the path has been successfully prefetched onPostPrefetchPathname(path); }); } return true; }, getPage: pathname => findPage(pathname), getResourceURLsForPathname: path => { const page = findPage(path); if (page) { return [...createComponentUrls(page.componentChunkName), createJsonURL(jsonDataPaths[page.jsonName])]; } else { return null; } }, getResourcesForPathnameSync: path => { const page = findPage(path); if (page) { return pathScriptsCache[page.path]; } else if (shouldFallbackTo404Resources(path)) { return queue.getResourcesForPathnameSync(`/404.html`); } else { return null; } }, // Get resources (code/data) for a path. Fetches metdata first // if necessary and then the code/data bundles. Used for prefetching // and getting resources for page changes. getResourcesForPathname: path => new Promise((resolve, reject) => { // Production code path if (failedPaths[path]) { handleResourceLoadError(path, `Previously detected load failure for "${path}"`); reject(); return; } const page = findPage(path); // In production, we lazy load page metadata. If that // hasn't been fetched yet, start fetching it now. if (!page && !fetchedPageResourceMap && process.env.NODE_ENV === `production`) { // If page wasn't found check and we didn't fetch resources map for // all pages, wait for fetch to complete and try to get resources again fetchPageResourceMap().then(() => resolve(queue.getResourcesForPathname(path))); return; } if (!page) { if (shouldFallbackTo404Resources(path)) { console.log(`A page wasn't found for "${path}"`); // Preload the custom 404 page resolve(queue.getResourcesForPathname(`/404.html`)); return; } resolve(); return; } // Use the path from the page so the pathScriptsCache uses // the normalized path. path = page.path; // Check if it's in the cache already. if (pathScriptsCache[path]) { _emitter.default.emit(`onPostLoadPageResources`, { page, pageResources: pathScriptsCache[path] }); resolve(pathScriptsCache[path]); return; } // Nope, we need to load resource(s) _emitter.default.emit(`onPreLoadPageResources`, { path }); // In development we know the code is loaded already // so we just return with it immediately. if (process.env.NODE_ENV !== `production`) { const pageResources = { component: syncRequires.components[page.componentChunkName], page // Add to the cache. }; pathScriptsCache[path] = pageResources; devGetPageData(page.path).then(pageData => { _emitter.default.emit(`onPostLoadPageResources`, { page, pageResources }); // Tell plugins the path has been successfully prefetched onPostPrefetchPathname(path); resolve(pageResources); }); } else { Promise.all([getResourceModule(page.componentChunkName), getResourceModule(page.jsonName)]).then(([component, json]) => { if (!(component && json)) { resolve(null); return; } const pageResources = { component, json, page }; pageResources.page.jsonURL = createJsonURL(jsonDataPaths[page.jsonName]); pathScriptsCache[path] = pageResources; resolve(pageResources); _emitter.default.emit(`onPostLoadPageResources`, { page, pageResources }); // Tell plugins the path has been successfully prefetched onPostPrefetchPathname(path); }); } }) }; const postInitialRenderWork = () => { inInitialRender = false; if (process.env.NODE_ENV === `production`) { // We got all resources needed for first mount, // we can fetch resoures for all pages. fetchPageResourceMap(); } }; exports.postInitialRenderWork = postInitialRenderWork; const setApiRunnerForLoader = runner => { apiRunner = runner; disableCorePrefetching = apiRunner(`disableCorePrefetching`); }; exports.setApiRunnerForLoader = setApiRunnerForLoader; const publicLoader = { getResourcesForPathname: queue.getResourcesForPathname, getResourceURLsForPathname: queue.getResourceURLsForPathname, getResourcesForPathnameSync: queue.getResourcesForPathnameSync }; exports.publicLoader = publicLoader; var _default = queue; exports.default = _default;