cl-react-graph
Version:
420 lines (345 loc) • 13.2 kB
JavaScript
;
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;