UNPKG

next-csrf

Version:

CSRF mitigation library for Next.js

62 lines 12.7 kB
"use strict";var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");exports.__esModule=true;exports.default=exports.looseToArray=void 0;var _mitt=_interopRequireDefault(require("../next-server/lib/mitt"));var _router=require("../next-server/lib/router/router");var _escapePathDelimiters=_interopRequireDefault(require("../next-server/lib/router/utils/escape-path-delimiters"));var _getAssetPathFromRoute=_interopRequireDefault(require("../next-server/lib/router/utils/get-asset-path-from-route"));var _isDynamic=require("../next-server/lib/router/utils/is-dynamic");var _parseRelativeUrl=require("../next-server/lib/router/utils/parse-relative-url");var _querystring=require("../next-server/lib/router/utils/querystring");var _routeMatcher=require("../next-server/lib/router/utils/route-matcher");var _routeRegex=require("../next-server/lib/router/utils/route-regex");const looseToArray=input=>[].slice.call(input);exports.looseToArray=looseToArray;function getInitialStylesheets(){return looseToArray(document.styleSheets).filter(el=>el.ownerNode&&el.ownerNode.tagName==='LINK'&&el.ownerNode.hasAttribute('data-n-p')).map(sheet=>({href:sheet.ownerNode.getAttribute('href'),text:looseToArray(sheet.cssRules).map(r=>r.cssText).join('')}));}function hasRel(rel,link){try{link=document.createElement('link');return link.relList.supports(rel);}catch(_unused){}}function pageLoadError(route){return(0,_router.markLoadingError)(new Error(`Error loading ${route}`));}const relPrefetch=hasRel('preload')&&!hasRel('prefetch')?// https://caniuse.com/#feat=link-rel-preload // macOS and iOS (Safari does not support prefetch) 'preload':// https://caniuse.com/#feat=link-rel-prefetch // IE 11, Edge 12+, nearly all evergreen 'prefetch';const relPreload=hasRel('preload')?'preload':relPrefetch;const relPreloadStyle='fetch';const hasNoModule=('noModule'in document.createElement('script'));function normalizeRoute(route){if(route[0]!=='/'){throw new Error(`Route name should start with a "/", got "${route}"`);}if(route==='/')return route;return route.replace(/\/$/,'');}function appendLink(href,rel,as,link){return new Promise((res,rej)=>{link=document.createElement('link');// The order of property assignment here is intentional: if(as)link.as=as;link.rel=rel;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);});}function loadScript(url){return new Promise((res,rej)=>{const script=document.createElement('script');if(process.env.__NEXT_MODERN_BUILD&&hasNoModule){script.type='module';}script.crossOrigin=process.env.__NEXT_CROSS_ORIGIN;script.src=url;script.onload=res;script.onerror=()=>rej(pageLoadError(url));document.body.appendChild(script);});}class PageLoader{constructor(buildId,assetPrefix,initialPage){this.initialPage=void 0;this.buildId=void 0;this.assetPrefix=void 0;this.pageCache=void 0;this.pageRegisterEvents=void 0;this.loadingRoutes=void 0;this.promisedBuildManifest=void 0;this.promisedSsgManifest=void 0;this.promisedDevPagesManifest=void 0;this.initialPage=initialPage;this.buildId=buildId;this.assetPrefix=assetPrefix;this.pageCache={};this.pageRegisterEvents=(0,_mitt.default)();this.loadingRoutes={// By default these 2 pages are being loaded in the initial html '/_app':true};// TODO: get rid of this limitation for rendering the error page if(initialPage!=='/_error'){this.loadingRoutes[initialPage]=true;}this.promisedBuildManifest=new Promise(resolve=>{if(window.__BUILD_MANIFEST){resolve(window.__BUILD_MANIFEST);}else{;window.__BUILD_MANIFEST_CB=()=>{resolve(window.__BUILD_MANIFEST);};}});/** @type {Promise<Set<string>>} */this.promisedSsgManifest=new Promise(resolve=>{if(window.__SSG_MANIFEST){resolve(window.__SSG_MANIFEST);}else{;window.__SSG_MANIFEST_CB=()=>{resolve(window.__SSG_MANIFEST);};}});}getPageList(){if(process.env.NODE_ENV==='production'){return this.promisedBuildManifest.then(buildManifest=>buildManifest.sortedPages);}else{if(window.__DEV_PAGES_MANIFEST){return window.__DEV_PAGES_MANIFEST.pages;}else{if(!this.promisedDevPagesManifest){this.promisedDevPagesManifest=fetch(`${this.assetPrefix}/_next/static/development/_devPagesManifest.json`).then(res=>res.json()).then(manifest=>{;window.__DEV_PAGES_MANIFEST=manifest;return manifest.pages;}).catch(err=>{console.log(`Failed to fetch devPagesManifest`,err);});}return this.promisedDevPagesManifest;}}}// Returns a promise for the dependencies for a particular route getDependencies(route){return this.promisedBuildManifest.then(m=>{return m[route]?m[route].map(url=>`${this.assetPrefix}/_next/${encodeURI(url)}`):Promise.reject(pageLoadError(route));});}/** * @param {string} href the route href (file-system path) * @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes */getDataHref(href,asPath,ssg){const{pathname:hrefPathname,searchParams,search}=(0,_parseRelativeUrl.parseRelativeUrl)(href);const query=(0,_querystring.searchParamsToUrlQuery)(searchParams);const{pathname:asPathname}=(0,_parseRelativeUrl.parseRelativeUrl)(asPath);const route=normalizeRoute(hrefPathname);const getHrefForSlug=path=>{const dataRoute=(0,_getAssetPathFromRoute.default)(path,'.json');return(0,_router.addBasePath)(`/_next/data/${this.buildId}${dataRoute}${ssg?'':search}`);};let isDynamic=(0,_isDynamic.isDynamicRoute)(route),interpolatedRoute;if(isDynamic){const dynamicRegex=(0,_routeRegex.getRouteRegex)(route);const dynamicGroups=dynamicRegex.groups;const dynamicMatches=// Try to match the dynamic route against the asPath (0,_routeMatcher.getRouteMatcher)(dynamicRegex)(asPathname)||// Fall back to reading the values from the href // TODO: should this take priority; also need to change in the router. query;interpolatedRoute=route;if(!Object.keys(dynamicGroups).every(param=>{let value=dynamicMatches[param]||'';const{repeat,optional}=dynamicGroups[param];// support single-level catch-all // TODO: more robust handling for user-error (passing `/`) let replaced=`[${repeat?'...':''}${param}]`;if(optional){replaced=`${!value?'/':''}[${replaced}]`;}if(repeat&&!Array.isArray(value))value=[value];return(optional||param in dynamicMatches)&&(// Interpolate group into data URL if present interpolatedRoute=interpolatedRoute.replace(replaced,repeat?value.map(_escapePathDelimiters.default).join('/'):(0,_escapePathDelimiters.default)(value))||'/');})){interpolatedRoute='';// did not satisfy all requirements // n.b. We ignore this error because we handle warning for this case in // development in the `<Link>` component directly. }}return isDynamic?interpolatedRoute&&getHrefForSlug(interpolatedRoute):getHrefForSlug(route);}/** * @param {string} href the route href (file-system path) * @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes */prefetchData(href,asPath){const{pathname:hrefPathname}=(0,_parseRelativeUrl.parseRelativeUrl)(href);const route=normalizeRoute(hrefPathname);return this.promisedSsgManifest.then((s,_dataHref)=>// Check if the route requires a data file s.has(route)&&(// Try to generate data href, noop when falsy _dataHref=this.getDataHref(href,asPath,true))&&// noop when data has already been prefetched (dedupe) !document.querySelector(`link[rel="${relPrefetch}"][href^="${_dataHref}"]`)&&// Inject the `<link rel=prefetch>` tag for above computed `href`. appendLink(_dataHref,relPrefetch,'fetch').catch(()=>{/* ignore prefetch error */}));}loadPage(route){route=normalizeRoute(route);return new Promise((resolve,reject)=>{// If there's a cached version of the page, let's use it. const cachedPage=this.pageCache[route];if(cachedPage){if('error'in cachedPage){reject(cachedPage.error);}else{resolve(cachedPage);}return;}const fire=pageToCache=>{this.pageRegisterEvents.off(route,fire);delete this.loadingRoutes[route];if('error'in pageToCache){reject(pageToCache.error);}else{resolve(pageToCache);}};// Register a listener to get the page this.pageRegisterEvents.on(route,fire);if(!this.loadingRoutes[route]){this.loadingRoutes[route]=true;if(process.env.NODE_ENV==='production'){this.getDependencies(route).then(deps=>{const pending=[];deps.forEach(d=>{if(d.endsWith('.js')&&!document.querySelector(`script[src^="${d}"]`)){pending.push(loadScript(d));}// Prefetch CSS as it'll be needed when the page JavaScript // evaluates. This will only trigger if explicit prefetching is // disabled for a <Link>... prefetching in this case is desirable // because we *know* it's going to be used very soon (page was // loaded). if(d.endsWith('.css')&&!document.querySelector(`link[rel="${relPreload}"][href^="${d}"]`)){// This is not pushed into `pending` because we don't need to // wait for these to resolve. To prevent an unhandled // rejection, we swallow the error which is handled later in // the rendering cycle (this is just a preload optimization). appendLink(d,relPreload,relPreloadStyle).catch(()=>{/* ignore preload error */});}});return Promise.all(pending);}).catch(err=>{// Mark the page as failed to load if any of its required scripts // fail to load: this.pageCache[route]={error:err};fire({error:err});});}else{// Development only. In production the page file is part of the build manifest route=normalizeRoute(route);let scriptRoute=(0,_getAssetPathFromRoute.default)(route,'.js');const url=`${this.assetPrefix}/_next/static/chunks/pages${encodeURI(scriptRoute)}`;loadScript(url).catch(err=>{// Mark the page as failed to load if its script fails to load: this.pageCache[route]={error:err};fire({error:err});});}}});}// This method if called by the route code. registerPage(route,regFn){const register=styleSheets=>{try{const mod=regFn();const pageData={page:mod.default||mod,mod,styleSheets};this.pageCache[route]=pageData;this.pageRegisterEvents.emit(route,pageData);}catch(error){this.pageCache[route]={error};this.pageRegisterEvents.emit(route,{error});}};if(process.env.NODE_ENV!=='production'){// Wait for webpack to become idle if it's not. // More info: https://github.com/vercel/next.js/pull/1511 if(module.hot&&module.hot.status()!=='idle'){console.log(`Waiting for webpack to become "idle" to initialize the page: "${route}"`);const check=status=>{if(status==='idle'){;module.hot.removeStatusHandler(check);register(/* css is handled via style-loader in development */[]);}};module.hot.status(check);return;}}function fetchStyleSheet(href){return fetch(href).then(res=>{if(!res.ok)throw pageLoadError(href);return res.text().then(text=>({href,text}));});}const isInitialLoad=route===this.initialPage;const promisedDeps=// Shared styles will already be on the page: route==='/_app'||// We use `style-loader` in development: process.env.NODE_ENV!=='production'?Promise.resolve([]):// Tests that this does not block hydration: // test/integration/css-fixtures/hydrate-without-deps/ (isInitialLoad?Promise.resolve(looseToArray(document.querySelectorAll('link[data-n-p]')).map(e=>e.getAttribute('href'))):this.getDependencies(route).then(deps=>deps.filter(d=>d.endsWith('.css')))).then(cssFiles=>// These files should've already been fetched by now, so this // should resolve instantly. Promise.all(cssFiles.map(d=>fetchStyleSheet(d))).catch(err=>{if(isInitialLoad)return getInitialStylesheets();throw err;}));promisedDeps.then(deps=>register(deps),error=>{this.pageCache[route]={error};this.pageRegisterEvents.emit(route,{error});});}/** * @param {string} route * @param {boolean} [isDependency] */prefetch(route,isDependency){// 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();}/** @type {string} */let url;if(isDependency){url=route;}else{if(process.env.NODE_ENV!=='production'){route=normalizeRoute(route);const ext=process.env.__NEXT_MODERN_BUILD&&hasNoModule?'.module.js':'.js';const scriptRoute=(0,_getAssetPathFromRoute.default)(route,ext);url=`${this.assetPrefix}/_next/static/${encodeURIComponent(this.buildId)}/pages${encodeURI(scriptRoute)}`;}}return Promise.all(document.querySelector(`link[rel="${relPrefetch}"][href^="${url}"]`)?[]:[url&&appendLink(url,relPrefetch,url.endsWith('.css')?relPreloadStyle:'script'),process.env.NODE_ENV==='production'&&!isDependency&&this.getDependencies(route).then(urls=>Promise.all(urls.map(dependencyUrl=>this.prefetch(dependencyUrl,true))))]).then(// do not return any data ()=>{},// swallow prefetch errors ()=>{});}}exports.default=PageLoader; //# sourceMappingURL=page-loader.js.map