UNPKG

next

Version:

The React Framework

319 lines (318 loc) • 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.markAssetError = markAssetError; exports.isAssetError = isAssetError; exports.getClientBuildManifest = getClientBuildManifest; exports.getMiddlewareManifest = getMiddlewareManifest; exports.createRouteLoader = createRouteLoader; var _getAssetPathFromRoute = _interopRequireDefault(require("../shared/lib/router/utils/get-asset-path-from-route")); var _requestIdleCallback = require("./request-idle-callback"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // 3.8s was arbitrarily chosen as it's what https://web.dev/interactive // considers as "Good" time-to-interactive. We must assume something went // wrong beyond this point, and then fall-back to a full page transition to // show the user something of value. const MS_MAX_IDLE_DELAY = 3800; function withFuture(key, map, generator) { let entry = map.get(key); if (entry) { if ('future' in entry) { return entry.future; } return Promise.resolve(entry); } let resolver; const prom = new Promise((resolve)=>{ resolver = resolve; }); map.set(key, entry = { resolve: resolver, future: prom }); return generator ? generator()// eslint-disable-next-line no-sequences .then((value)=>(resolver(value), value) ).catch((err)=>{ map.delete(key); throw err; }) : prom; } function hasPrefetch(link) { try { link = document.createElement('link'); return(// detect IE11 since it supports prefetch but isn't detected // with relList.support (!!window.MSInputMethodContext && !!document.documentMode) || link.relList.supports('prefetch')); } catch (e) { return false; } } const canPrefetch = hasPrefetch(); function prefetchViaDom(href, as, link) { return new Promise((res, rej)=>{ const selector = ` link[rel="prefetch"][href^="${href}"], link[rel="preload"][href^="${href}"], script[src^="${href}"]`; if (document.querySelector(selector)) { return res(); } link = document.createElement('link'); // The order of property assignment here is intentional: if (as) link.as = as; link.rel = `prefetch`; link.crossOrigin = process.env.__NEXT_CROSS_ORIGIN; link.onload = res; link.onerror = rej; // `href` should always be last: link.href = href; document.head.appendChild(link); }); } const ASSET_LOAD_ERROR = Symbol('ASSET_LOAD_ERROR'); function markAssetError(err) { return Object.defineProperty(err, ASSET_LOAD_ERROR, {}); } function isAssetError(err) { return err && ASSET_LOAD_ERROR in err; } function appendScript(src, script) { return new Promise((resolve, reject)=>{ script = document.createElement('script'); // The order of property assignment here is intentional. // 1. Setup success/failure hooks in case the browser synchronously // executes when `src` is set. script.onload = resolve; script.onerror = ()=>reject(markAssetError(new Error(`Failed to load script: ${src}`))) ; // 2. Configure the cross-origin attribute before setting `src` in case the // browser begins to fetch. script.crossOrigin = process.env.__NEXT_CROSS_ORIGIN; // 3. Finally, set the source and inject into the DOM in case the child // must be appended for fetching to start. script.src = src; document.body.appendChild(script); }); } // We wait for pages to be built in dev before we start the route transition // timeout to prevent an un-necessary hard navigation in development. let devBuildPromise; // Resolve a promise that times out after given amount of milliseconds. function resolvePromiseWithTimeout(p, ms, err) { return new Promise((resolve, reject)=>{ let cancelled = false; p.then((r)=>{ // Resolved, cancel the timeout cancelled = true; resolve(r); }).catch(reject); // We wrap these checks separately for better dead-code elimination in // production bundles. if (process.env.NODE_ENV === 'development') { (devBuildPromise || Promise.resolve()).then(()=>{ (0, _requestIdleCallback).requestIdleCallback(()=>setTimeout(()=>{ if (!cancelled) { reject(err); } }, ms) ); }); } if (process.env.NODE_ENV !== 'development') { (0, _requestIdleCallback).requestIdleCallback(()=>setTimeout(()=>{ if (!cancelled) { reject(err); } }, ms) ); } }); } function getClientBuildManifest() { if (self.__BUILD_MANIFEST) { return Promise.resolve(self.__BUILD_MANIFEST); } const onBuildManifest = new Promise((resolve)=>{ // Mandatory because this is not concurrent safe: const cb = self.__BUILD_MANIFEST_CB; self.__BUILD_MANIFEST_CB = ()=>{ resolve(self.__BUILD_MANIFEST); cb && cb(); }; }); return resolvePromiseWithTimeout(onBuildManifest, MS_MAX_IDLE_DELAY, markAssetError(new Error('Failed to load client build manifest'))); } function getMiddlewareManifest() { if (self.__MIDDLEWARE_MANIFEST) { return Promise.resolve(self.__MIDDLEWARE_MANIFEST); } const onMiddlewareManifest = new Promise((resolve)=>{ const cb = self.__MIDDLEWARE_MANIFEST_CB; self.__MIDDLEWARE_MANIFEST_CB = ()=>{ resolve(self.__MIDDLEWARE_MANIFEST); cb && cb(); }; }); return resolvePromiseWithTimeout(onMiddlewareManifest, MS_MAX_IDLE_DELAY, markAssetError(new Error('Failed to load client middleware manifest'))); } function getFilesForRoute(assetPrefix, route) { if (process.env.NODE_ENV === 'development') { return Promise.resolve({ scripts: [ assetPrefix + '/_next/static/chunks/pages' + encodeURI((0, _getAssetPathFromRoute).default(route, '.js')), ], // Styles are handled by `style-loader` in development: css: [] }); } return getClientBuildManifest().then((manifest)=>{ if (!(route in manifest)) { throw markAssetError(new Error(`Failed to lookup route: ${route}`)); } const allFiles = manifest[route].map((entry)=>assetPrefix + '/_next/' + encodeURI(entry) ); return { scripts: allFiles.filter((v)=>v.endsWith('.js') ), css: allFiles.filter((v)=>v.endsWith('.css') ) }; }); } function createRouteLoader(assetPrefix) { const entrypoints = new Map(); const loadedScripts = new Map(); const styleSheets = new Map(); const routes = new Map(); function maybeExecuteScript(src) { // With HMR we might need to "reload" scripts when they are // disposed and readded. Executing scripts twice has no functional // differences if (process.env.NODE_ENV !== 'development') { let prom = loadedScripts.get(src); if (prom) { return prom; } // Skip executing script if it's already in the DOM: if (document.querySelector(`script[src^="${src}"]`)) { return Promise.resolve(); } loadedScripts.set(src, prom = appendScript(src)); return prom; } else { return appendScript(src); } } function fetchStyleSheet(href) { let prom = styleSheets.get(href); if (prom) { return prom; } styleSheets.set(href, prom = fetch(href).then((res)=>{ if (!res.ok) { throw new Error(`Failed to load stylesheet: ${href}`); } return res.text().then((text)=>({ href: href, content: text }) ); }).catch((err)=>{ throw markAssetError(err); })); return prom; } return { whenEntrypoint (route) { return withFuture(route, entrypoints); }, onEntrypoint (route, execute) { (execute ? Promise.resolve().then(()=>execute() ).then((exports)=>({ component: exports && exports.default || exports, exports: exports }) , (err)=>({ error: err }) ) : Promise.resolve(undefined)).then((input)=>{ const old = entrypoints.get(route); if (old && 'resolve' in old) { if (input) { entrypoints.set(route, input); old.resolve(input); } } else { if (input) { entrypoints.set(route, input); } else { entrypoints.delete(route); } // when this entrypoint has been resolved before // the route is outdated and we want to invalidate // this cache entry routes.delete(route); } }); }, loadRoute (route, prefetch) { return withFuture(route, routes, ()=>{ let devBuildPromiseResolve; if (process.env.NODE_ENV === 'development') { devBuildPromise = new Promise((resolve)=>{ devBuildPromiseResolve = resolve; }); } return resolvePromiseWithTimeout(getFilesForRoute(assetPrefix, route).then(({ scripts , css })=>{ return Promise.all([ entrypoints.has(route) ? [] : Promise.all(scripts.map(maybeExecuteScript)), Promise.all(css.map(fetchStyleSheet)), ]); }).then((res)=>{ return this.whenEntrypoint(route).then((entrypoint)=>({ entrypoint, styles: res[1] }) ); }), MS_MAX_IDLE_DELAY, markAssetError(new Error(`Route did not complete loading: ${route}`))).then(({ entrypoint , styles })=>{ const res = Object.assign({ styles: styles }, entrypoint); return 'error' in entrypoint ? entrypoint : res; }).catch((err)=>{ if (prefetch) { // we don't want to cache errors during prefetch throw err; } return { error: err }; }).finally(()=>{ return devBuildPromiseResolve === null || devBuildPromiseResolve === void 0 ? void 0 : devBuildPromiseResolve(); }); }); }, prefetch (route) { // https://github.com/GoogleChromeLabs/quicklink/blob/453a661fa1fa940e2d2e044452398e38c67a98fb/src/index.mjs#L115-L118 // License: Apache 2.0 let cn; if (cn = navigator.connection) { // Don't prefetch if using 2G or if Save-Data is enabled. if (cn.saveData || /2g/.test(cn.effectiveType)) return Promise.resolve(); } return getFilesForRoute(assetPrefix, route).then((output)=>Promise.all(canPrefetch ? output.scripts.map((script)=>prefetchViaDom(script, 'script') ) : []) ).then(()=>{ (0, _requestIdleCallback).requestIdleCallback(()=>this.loadRoute(route, true).catch(()=>{}) ); }).catch(// swallow prefetch errors ()=>{}); } }; } //# sourceMappingURL=route-loader.js.map