1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038 |
- /**
- * @remix-run/router v1.23.0
- *
- * Copyright (c) Remix Software Inc.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.md file in the root directory of this source tree.
- *
- * @license MIT
- */
- function _extends() {
- _extends = Object.assign ? Object.assign.bind() : function (target) {
- for (var i = 1; i < arguments.length; i++) {
- var source = arguments[i];
- for (var key in source) {
- if (Object.prototype.hasOwnProperty.call(source, key)) {
- target[key] = source[key];
- }
- }
- }
- return target;
- };
- return _extends.apply(this, arguments);
- }
- ////////////////////////////////////////////////////////////////////////////////
- //#region Types and Constants
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * Actions represent the type of change to a location value.
- */
- var Action;
- (function (Action) {
- /**
- * A POP indicates a change to an arbitrary index in the history stack, such
- * as a back or forward navigation. It does not describe the direction of the
- * navigation, only that the current index changed.
- *
- * Note: This is the default action for newly created history objects.
- */
- Action["Pop"] = "POP";
- /**
- * A PUSH indicates a new entry being added to the history stack, such as when
- * a link is clicked and a new page loads. When this happens, all subsequent
- * entries in the stack are lost.
- */
- Action["Push"] = "PUSH";
- /**
- * A REPLACE indicates the entry at the current index in the history stack
- * being replaced by a new one.
- */
- Action["Replace"] = "REPLACE";
- })(Action || (Action = {}));
- const PopStateEventType = "popstate";
- /**
- * Memory history stores the current location in memory. It is designed for use
- * in stateful non-browser environments like tests and React Native.
- */
- function createMemoryHistory(options) {
- if (options === void 0) {
- options = {};
- }
- let {
- initialEntries = ["/"],
- initialIndex,
- v5Compat = false
- } = options;
- let entries; // Declare so we can access from createMemoryLocation
- entries = initialEntries.map((entry, index) => createMemoryLocation(entry, typeof entry === "string" ? null : entry.state, index === 0 ? "default" : undefined));
- let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex);
- let action = Action.Pop;
- let listener = null;
- function clampIndex(n) {
- return Math.min(Math.max(n, 0), entries.length - 1);
- }
- function getCurrentLocation() {
- return entries[index];
- }
- function createMemoryLocation(to, state, key) {
- if (state === void 0) {
- state = null;
- }
- let location = createLocation(entries ? getCurrentLocation().pathname : "/", to, state, key);
- warning(location.pathname.charAt(0) === "/", "relative pathnames are not supported in memory history: " + JSON.stringify(to));
- return location;
- }
- function createHref(to) {
- return typeof to === "string" ? to : createPath(to);
- }
- let history = {
- get index() {
- return index;
- },
- get action() {
- return action;
- },
- get location() {
- return getCurrentLocation();
- },
- createHref,
- createURL(to) {
- return new URL(createHref(to), "http://localhost");
- },
- encodeLocation(to) {
- let path = typeof to === "string" ? parsePath(to) : to;
- return {
- pathname: path.pathname || "",
- search: path.search || "",
- hash: path.hash || ""
- };
- },
- push(to, state) {
- action = Action.Push;
- let nextLocation = createMemoryLocation(to, state);
- index += 1;
- entries.splice(index, entries.length, nextLocation);
- if (v5Compat && listener) {
- listener({
- action,
- location: nextLocation,
- delta: 1
- });
- }
- },
- replace(to, state) {
- action = Action.Replace;
- let nextLocation = createMemoryLocation(to, state);
- entries[index] = nextLocation;
- if (v5Compat && listener) {
- listener({
- action,
- location: nextLocation,
- delta: 0
- });
- }
- },
- go(delta) {
- action = Action.Pop;
- let nextIndex = clampIndex(index + delta);
- let nextLocation = entries[nextIndex];
- index = nextIndex;
- if (listener) {
- listener({
- action,
- location: nextLocation,
- delta
- });
- }
- },
- listen(fn) {
- listener = fn;
- return () => {
- listener = null;
- };
- }
- };
- return history;
- }
- /**
- * Browser history stores the location in regular URLs. This is the standard for
- * most web apps, but it requires some configuration on the server to ensure you
- * serve the same app at multiple URLs.
- *
- * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory
- */
- function createBrowserHistory(options) {
- if (options === void 0) {
- options = {};
- }
- function createBrowserLocation(window, globalHistory) {
- let {
- pathname,
- search,
- hash
- } = window.location;
- return createLocation("", {
- pathname,
- search,
- hash
- },
- // state defaults to `null` because `window.history.state` does
- globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");
- }
- function createBrowserHref(window, to) {
- return typeof to === "string" ? to : createPath(to);
- }
- return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);
- }
- /**
- * Hash history stores the location in window.location.hash. This makes it ideal
- * for situations where you don't want to send the location to the server for
- * some reason, either because you do cannot configure it or the URL space is
- * reserved for something else.
- *
- * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory
- */
- function createHashHistory(options) {
- if (options === void 0) {
- options = {};
- }
- function createHashLocation(window, globalHistory) {
- let {
- pathname = "/",
- search = "",
- hash = ""
- } = parsePath(window.location.hash.substr(1));
- // Hash URL should always have a leading / just like window.location.pathname
- // does, so if an app ends up at a route like /#something then we add a
- // leading slash so all of our path-matching behaves the same as if it would
- // in a browser router. This is particularly important when there exists a
- // root splat route (<Route path="*">) since that matches internally against
- // "/*" and we'd expect /#something to 404 in a hash router app.
- if (!pathname.startsWith("/") && !pathname.startsWith(".")) {
- pathname = "/" + pathname;
- }
- return createLocation("", {
- pathname,
- search,
- hash
- },
- // state defaults to `null` because `window.history.state` does
- globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");
- }
- function createHashHref(window, to) {
- let base = window.document.querySelector("base");
- let href = "";
- if (base && base.getAttribute("href")) {
- let url = window.location.href;
- let hashIndex = url.indexOf("#");
- href = hashIndex === -1 ? url : url.slice(0, hashIndex);
- }
- return href + "#" + (typeof to === "string" ? to : createPath(to));
- }
- function validateHashLocation(location, to) {
- warning(location.pathname.charAt(0) === "/", "relative pathnames are not supported in hash history.push(" + JSON.stringify(to) + ")");
- }
- return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options);
- }
- function invariant(value, message) {
- if (value === false || value === null || typeof value === "undefined") {
- throw new Error(message);
- }
- }
- function warning(cond, message) {
- if (!cond) {
- // eslint-disable-next-line no-console
- if (typeof console !== "undefined") console.warn(message);
- try {
- // Welcome to debugging history!
- //
- // This error is thrown as a convenience, so you can more easily
- // find the source for a warning that appears in the console by
- // enabling "pause on exceptions" in your JavaScript debugger.
- throw new Error(message);
- // eslint-disable-next-line no-empty
- } catch (e) {}
- }
- }
- function createKey() {
- return Math.random().toString(36).substr(2, 8);
- }
- /**
- * For browser-based histories, we combine the state and key into an object
- */
- function getHistoryState(location, index) {
- return {
- usr: location.state,
- key: location.key,
- idx: index
- };
- }
- /**
- * Creates a Location object with a unique key from the given Path
- */
- function createLocation(current, to, state, key) {
- if (state === void 0) {
- state = null;
- }
- let location = _extends({
- pathname: typeof current === "string" ? current : current.pathname,
- search: "",
- hash: ""
- }, typeof to === "string" ? parsePath(to) : to, {
- state,
- // TODO: This could be cleaned up. push/replace should probably just take
- // full Locations now and avoid the need to run through this flow at all
- // But that's a pretty big refactor to the current test suite so going to
- // keep as is for the time being and just let any incoming keys take precedence
- key: to && to.key || key || createKey()
- });
- return location;
- }
- /**
- * Creates a string URL path from the given pathname, search, and hash components.
- */
- function createPath(_ref) {
- let {
- pathname = "/",
- search = "",
- hash = ""
- } = _ref;
- if (search && search !== "?") pathname += search.charAt(0) === "?" ? search : "?" + search;
- if (hash && hash !== "#") pathname += hash.charAt(0) === "#" ? hash : "#" + hash;
- return pathname;
- }
- /**
- * Parses a string URL path into its separate pathname, search, and hash components.
- */
- function parsePath(path) {
- let parsedPath = {};
- if (path) {
- let hashIndex = path.indexOf("#");
- if (hashIndex >= 0) {
- parsedPath.hash = path.substr(hashIndex);
- path = path.substr(0, hashIndex);
- }
- let searchIndex = path.indexOf("?");
- if (searchIndex >= 0) {
- parsedPath.search = path.substr(searchIndex);
- path = path.substr(0, searchIndex);
- }
- if (path) {
- parsedPath.pathname = path;
- }
- }
- return parsedPath;
- }
- function getUrlBasedHistory(getLocation, createHref, validateLocation, options) {
- if (options === void 0) {
- options = {};
- }
- let {
- window = document.defaultView,
- v5Compat = false
- } = options;
- let globalHistory = window.history;
- let action = Action.Pop;
- let listener = null;
- let index = getIndex();
- // Index should only be null when we initialize. If not, it's because the
- // user called history.pushState or history.replaceState directly, in which
- // case we should log a warning as it will result in bugs.
- if (index == null) {
- index = 0;
- globalHistory.replaceState(_extends({}, globalHistory.state, {
- idx: index
- }), "");
- }
- function getIndex() {
- let state = globalHistory.state || {
- idx: null
- };
- return state.idx;
- }
- function handlePop() {
- action = Action.Pop;
- let nextIndex = getIndex();
- let delta = nextIndex == null ? null : nextIndex - index;
- index = nextIndex;
- if (listener) {
- listener({
- action,
- location: history.location,
- delta
- });
- }
- }
- function push(to, state) {
- action = Action.Push;
- let location = createLocation(history.location, to, state);
- if (validateLocation) validateLocation(location, to);
- index = getIndex() + 1;
- let historyState = getHistoryState(location, index);
- let url = history.createHref(location);
- // try...catch because iOS limits us to 100 pushState calls :/
- try {
- globalHistory.pushState(historyState, "", url);
- } catch (error) {
- // If the exception is because `state` can't be serialized, let that throw
- // outwards just like a replace call would so the dev knows the cause
- // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
- // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
- if (error instanceof DOMException && error.name === "DataCloneError") {
- throw error;
- }
- // They are going to lose state here, but there is no real
- // way to warn them about it since the page will refresh...
- window.location.assign(url);
- }
- if (v5Compat && listener) {
- listener({
- action,
- location: history.location,
- delta: 1
- });
- }
- }
- function replace(to, state) {
- action = Action.Replace;
- let location = createLocation(history.location, to, state);
- if (validateLocation) validateLocation(location, to);
- index = getIndex();
- let historyState = getHistoryState(location, index);
- let url = history.createHref(location);
- globalHistory.replaceState(historyState, "", url);
- if (v5Compat && listener) {
- listener({
- action,
- location: history.location,
- delta: 0
- });
- }
- }
- function createURL(to) {
- // window.location.origin is "null" (the literal string value) in Firefox
- // under certain conditions, notably when serving from a local HTML file
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
- let base = window.location.origin !== "null" ? window.location.origin : window.location.href;
- let href = typeof to === "string" ? to : createPath(to);
- // Treating this as a full URL will strip any trailing spaces so we need to
- // pre-encode them since they might be part of a matching splat param from
- // an ancestor route
- href = href.replace(/ $/, "%20");
- invariant(base, "No window.location.(origin|href) available to create URL for href: " + href);
- return new URL(href, base);
- }
- let history = {
- get action() {
- return action;
- },
- get location() {
- return getLocation(window, globalHistory);
- },
- listen(fn) {
- if (listener) {
- throw new Error("A history only accepts one active listener");
- }
- window.addEventListener(PopStateEventType, handlePop);
- listener = fn;
- return () => {
- window.removeEventListener(PopStateEventType, handlePop);
- listener = null;
- };
- },
- createHref(to) {
- return createHref(window, to);
- },
- createURL,
- encodeLocation(to) {
- // Encode a Location the same way window.location would
- let url = createURL(to);
- return {
- pathname: url.pathname,
- search: url.search,
- hash: url.hash
- };
- },
- push,
- replace,
- go(n) {
- return globalHistory.go(n);
- }
- };
- return history;
- }
- //#endregion
- var ResultType;
- (function (ResultType) {
- ResultType["data"] = "data";
- ResultType["deferred"] = "deferred";
- ResultType["redirect"] = "redirect";
- ResultType["error"] = "error";
- })(ResultType || (ResultType = {}));
- const immutableRouteKeys = new Set(["lazy", "caseSensitive", "path", "id", "index", "children"]);
- function isIndexRoute(route) {
- return route.index === true;
- }
- // Walk the route tree generating unique IDs where necessary, so we are working
- // solely with AgnosticDataRouteObject's within the Router
- function convertRoutesToDataRoutes(routes, mapRouteProperties, parentPath, manifest) {
- if (parentPath === void 0) {
- parentPath = [];
- }
- if (manifest === void 0) {
- manifest = {};
- }
- return routes.map((route, index) => {
- let treePath = [...parentPath, String(index)];
- let id = typeof route.id === "string" ? route.id : treePath.join("-");
- invariant(route.index !== true || !route.children, "Cannot specify children on an index route");
- invariant(!manifest[id], "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages");
- if (isIndexRoute(route)) {
- let indexRoute = _extends({}, route, mapRouteProperties(route), {
- id
- });
- manifest[id] = indexRoute;
- return indexRoute;
- } else {
- let pathOrLayoutRoute = _extends({}, route, mapRouteProperties(route), {
- id,
- children: undefined
- });
- manifest[id] = pathOrLayoutRoute;
- if (route.children) {
- pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, mapRouteProperties, treePath, manifest);
- }
- return pathOrLayoutRoute;
- }
- });
- }
- /**
- * Matches the given routes to a location and returns the match data.
- *
- * @see https://reactrouter.com/v6/utils/match-routes
- */
- function matchRoutes(routes, locationArg, basename) {
- if (basename === void 0) {
- basename = "/";
- }
- return matchRoutesImpl(routes, locationArg, basename, false);
- }
- function matchRoutesImpl(routes, locationArg, basename, allowPartial) {
- let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
- let pathname = stripBasename(location.pathname || "/", basename);
- if (pathname == null) {
- return null;
- }
- let branches = flattenRoutes(routes);
- rankRouteBranches(branches);
- let matches = null;
- for (let i = 0; matches == null && i < branches.length; ++i) {
- // Incoming pathnames are generally encoded from either window.location
- // or from router.navigate, but we want to match against the unencoded
- // paths in the route definitions. Memory router locations won't be
- // encoded here but there also shouldn't be anything to decode so this
- // should be a safe operation. This avoids needing matchRoutes to be
- // history-aware.
- let decoded = decodePath(pathname);
- matches = matchRouteBranch(branches[i], decoded, allowPartial);
- }
- return matches;
- }
- function convertRouteMatchToUiMatch(match, loaderData) {
- let {
- route,
- pathname,
- params
- } = match;
- return {
- id: route.id,
- pathname,
- params,
- data: loaderData[route.id],
- handle: route.handle
- };
- }
- function flattenRoutes(routes, branches, parentsMeta, parentPath) {
- if (branches === void 0) {
- branches = [];
- }
- if (parentsMeta === void 0) {
- parentsMeta = [];
- }
- if (parentPath === void 0) {
- parentPath = "";
- }
- let flattenRoute = (route, index, relativePath) => {
- let meta = {
- relativePath: relativePath === undefined ? route.path || "" : relativePath,
- caseSensitive: route.caseSensitive === true,
- childrenIndex: index,
- route
- };
- if (meta.relativePath.startsWith("/")) {
- invariant(meta.relativePath.startsWith(parentPath), "Absolute route path \"" + meta.relativePath + "\" nested under path " + ("\"" + parentPath + "\" is not valid. An absolute child route path ") + "must start with the combined path of all its parent routes.");
- meta.relativePath = meta.relativePath.slice(parentPath.length);
- }
- let path = joinPaths([parentPath, meta.relativePath]);
- let routesMeta = parentsMeta.concat(meta);
- // Add the children before adding this route to the array, so we traverse the
- // route tree depth-first and child routes appear before their parents in
- // the "flattened" version.
- if (route.children && route.children.length > 0) {
- invariant(
- // Our types know better, but runtime JS may not!
- // @ts-expect-error
- route.index !== true, "Index routes must not have child routes. Please remove " + ("all child routes from route path \"" + path + "\"."));
- flattenRoutes(route.children, branches, routesMeta, path);
- }
- // Routes without a path shouldn't ever match by themselves unless they are
- // index routes, so don't add them to the list of possible branches.
- if (route.path == null && !route.index) {
- return;
- }
- branches.push({
- path,
- score: computeScore(path, route.index),
- routesMeta
- });
- };
- routes.forEach((route, index) => {
- var _route$path;
- // coarse-grain check for optional params
- if (route.path === "" || !((_route$path = route.path) != null && _route$path.includes("?"))) {
- flattenRoute(route, index);
- } else {
- for (let exploded of explodeOptionalSegments(route.path)) {
- flattenRoute(route, index, exploded);
- }
- }
- });
- return branches;
- }
- /**
- * Computes all combinations of optional path segments for a given path,
- * excluding combinations that are ambiguous and of lower priority.
- *
- * For example, `/one/:two?/three/:four?/:five?` explodes to:
- * - `/one/three`
- * - `/one/:two/three`
- * - `/one/three/:four`
- * - `/one/three/:five`
- * - `/one/:two/three/:four`
- * - `/one/:two/three/:five`
- * - `/one/three/:four/:five`
- * - `/one/:two/three/:four/:five`
- */
- function explodeOptionalSegments(path) {
- let segments = path.split("/");
- if (segments.length === 0) return [];
- let [first, ...rest] = segments;
- // Optional path segments are denoted by a trailing `?`
- let isOptional = first.endsWith("?");
- // Compute the corresponding required segment: `foo?` -> `foo`
- let required = first.replace(/\?$/, "");
- if (rest.length === 0) {
- // Intepret empty string as omitting an optional segment
- // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
- return isOptional ? [required, ""] : [required];
- }
- let restExploded = explodeOptionalSegments(rest.join("/"));
- let result = [];
- // All child paths with the prefix. Do this for all children before the
- // optional version for all children, so we get consistent ordering where the
- // parent optional aspect is preferred as required. Otherwise, we can get
- // child sections interspersed where deeper optional segments are higher than
- // parent optional segments, where for example, /:two would explode _earlier_
- // then /:one. By always including the parent as required _for all children_
- // first, we avoid this issue
- result.push(...restExploded.map(subpath => subpath === "" ? required : [required, subpath].join("/")));
- // Then, if this is an optional value, add all child versions without
- if (isOptional) {
- result.push(...restExploded);
- }
- // for absolute paths, ensure `/` instead of empty segment
- return result.map(exploded => path.startsWith("/") && exploded === "" ? "/" : exploded);
- }
- function rankRouteBranches(branches) {
- branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
- : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));
- }
- const paramRe = /^:[\w-]+$/;
- const dynamicSegmentValue = 3;
- const indexRouteValue = 2;
- const emptySegmentValue = 1;
- const staticSegmentValue = 10;
- const splatPenalty = -2;
- const isSplat = s => s === "*";
- function computeScore(path, index) {
- let segments = path.split("/");
- let initialScore = segments.length;
- if (segments.some(isSplat)) {
- initialScore += splatPenalty;
- }
- if (index) {
- initialScore += indexRouteValue;
- }
- return segments.filter(s => !isSplat(s)).reduce((score, segment) => score + (paramRe.test(segment) ? dynamicSegmentValue : segment === "" ? emptySegmentValue : staticSegmentValue), initialScore);
- }
- function compareIndexes(a, b) {
- let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
- return siblings ?
- // If two routes are siblings, we should try to match the earlier sibling
- // first. This allows people to have fine-grained control over the matching
- // behavior by simply putting routes with identical paths in the order they
- // want them tried.
- a[a.length - 1] - b[b.length - 1] :
- // Otherwise, it doesn't really make sense to rank non-siblings by index,
- // so they sort equally.
- 0;
- }
- function matchRouteBranch(branch, pathname, allowPartial) {
- if (allowPartial === void 0) {
- allowPartial = false;
- }
- let {
- routesMeta
- } = branch;
- let matchedParams = {};
- let matchedPathname = "/";
- let matches = [];
- for (let i = 0; i < routesMeta.length; ++i) {
- let meta = routesMeta[i];
- let end = i === routesMeta.length - 1;
- let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
- let match = matchPath({
- path: meta.relativePath,
- caseSensitive: meta.caseSensitive,
- end
- }, remainingPathname);
- let route = meta.route;
- if (!match && end && allowPartial && !routesMeta[routesMeta.length - 1].route.index) {
- match = matchPath({
- path: meta.relativePath,
- caseSensitive: meta.caseSensitive,
- end: false
- }, remainingPathname);
- }
- if (!match) {
- return null;
- }
- Object.assign(matchedParams, match.params);
- matches.push({
- // TODO: Can this as be avoided?
- params: matchedParams,
- pathname: joinPaths([matchedPathname, match.pathname]),
- pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),
- route
- });
- if (match.pathnameBase !== "/") {
- matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
- }
- }
- return matches;
- }
- /**
- * Returns a path with params interpolated.
- *
- * @see https://reactrouter.com/v6/utils/generate-path
- */
- function generatePath(originalPath, params) {
- if (params === void 0) {
- params = {};
- }
- let path = originalPath;
- if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
- warning(false, "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
- path = path.replace(/\*$/, "/*");
- }
- // ensure `/` is added at the beginning if the path is absolute
- const prefix = path.startsWith("/") ? "/" : "";
- const stringify = p => p == null ? "" : typeof p === "string" ? p : String(p);
- const segments = path.split(/\/+/).map((segment, index, array) => {
- const isLastSegment = index === array.length - 1;
- // only apply the splat if it's the last segment
- if (isLastSegment && segment === "*") {
- const star = "*";
- // Apply the splat
- return stringify(params[star]);
- }
- const keyMatch = segment.match(/^:([\w-]+)(\??)$/);
- if (keyMatch) {
- const [, key, optional] = keyMatch;
- let param = params[key];
- invariant(optional === "?" || param != null, "Missing \":" + key + "\" param");
- return stringify(param);
- }
- // Remove any optional markers from optional static segments
- return segment.replace(/\?$/g, "");
- })
- // Remove empty segments
- .filter(segment => !!segment);
- return prefix + segments.join("/");
- }
- /**
- * Performs pattern matching on a URL pathname and returns information about
- * the match.
- *
- * @see https://reactrouter.com/v6/utils/match-path
- */
- function matchPath(pattern, pathname) {
- if (typeof pattern === "string") {
- pattern = {
- path: pattern,
- caseSensitive: false,
- end: true
- };
- }
- let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
- let match = pathname.match(matcher);
- if (!match) return null;
- let matchedPathname = match[0];
- let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
- let captureGroups = match.slice(1);
- let params = compiledParams.reduce((memo, _ref, index) => {
- let {
- paramName,
- isOptional
- } = _ref;
- // We need to compute the pathnameBase here using the raw splat value
- // instead of using params["*"] later because it will be decoded then
- if (paramName === "*") {
- let splatValue = captureGroups[index] || "";
- pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
- }
- const value = captureGroups[index];
- if (isOptional && !value) {
- memo[paramName] = undefined;
- } else {
- memo[paramName] = (value || "").replace(/%2F/g, "/");
- }
- return memo;
- }, {});
- return {
- params,
- pathname: matchedPathname,
- pathnameBase,
- pattern
- };
- }
- function compilePath(path, caseSensitive, end) {
- if (caseSensitive === void 0) {
- caseSensitive = false;
- }
- if (end === void 0) {
- end = true;
- }
- warning(path === "*" || !path.endsWith("*") || path.endsWith("/*"), "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
- let params = [];
- let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
- .replace(/^\/*/, "/") // Make sure it has a leading /
- .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
- .replace(/\/:([\w-]+)(\?)?/g, (_, paramName, isOptional) => {
- params.push({
- paramName,
- isOptional: isOptional != null
- });
- return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
- });
- if (path.endsWith("*")) {
- params.push({
- paramName: "*"
- });
- regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest
- : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
- } else if (end) {
- // When matching to the end, ignore trailing slashes
- regexpSource += "\\/*$";
- } else if (path !== "" && path !== "/") {
- // If our path is non-empty and contains anything beyond an initial slash,
- // then we have _some_ form of path in our regex, so we should expect to
- // match only if we find the end of this path segment. Look for an optional
- // non-captured trailing slash (to match a portion of the URL) or the end
- // of the path (if we've matched to the end). We used to do this with a
- // word boundary but that gives false positives on routes like
- // /user-preferences since `-` counts as a word boundary.
- regexpSource += "(?:(?=\\/|$))";
- } else ;
- let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
- return [matcher, params];
- }
- function decodePath(value) {
- try {
- return value.split("/").map(v => decodeURIComponent(v).replace(/\//g, "%2F")).join("/");
- } catch (error) {
- warning(false, "The URL path \"" + value + "\" could not be decoded because it is is a " + "malformed URL segment. This is probably due to a bad percent " + ("encoding (" + error + ")."));
- return value;
- }
- }
- /**
- * @private
- */
- function stripBasename(pathname, basename) {
- if (basename === "/") return pathname;
- if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
- return null;
- }
- // We want to leave trailing slash behavior in the user's control, so if they
- // specify a basename with a trailing slash, we should support it
- let startIndex = basename.endsWith("/") ? basename.length - 1 : basename.length;
- let nextChar = pathname.charAt(startIndex);
- if (nextChar && nextChar !== "/") {
- // pathname does not start with basename/
- return null;
- }
- return pathname.slice(startIndex) || "/";
- }
- /**
- * Returns a resolved path object relative to the given pathname.
- *
- * @see https://reactrouter.com/v6/utils/resolve-path
- */
- function resolvePath(to, fromPathname) {
- if (fromPathname === void 0) {
- fromPathname = "/";
- }
- let {
- pathname: toPathname,
- search = "",
- hash = ""
- } = typeof to === "string" ? parsePath(to) : to;
- let pathname = toPathname ? toPathname.startsWith("/") ? toPathname : resolvePathname(toPathname, fromPathname) : fromPathname;
- return {
- pathname,
- search: normalizeSearch(search),
- hash: normalizeHash(hash)
- };
- }
- function resolvePathname(relativePath, fromPathname) {
- let segments = fromPathname.replace(/\/+$/, "").split("/");
- let relativeSegments = relativePath.split("/");
- relativeSegments.forEach(segment => {
- if (segment === "..") {
- // Keep the root "" segment so the pathname starts at /
- if (segments.length > 1) segments.pop();
- } else if (segment !== ".") {
- segments.push(segment);
- }
- });
- return segments.length > 1 ? segments.join("/") : "/";
- }
- function getInvalidPathError(char, field, dest, path) {
- return "Cannot include a '" + char + "' character in a manually specified " + ("`to." + field + "` field [" + JSON.stringify(path) + "]. Please separate it out to the ") + ("`to." + dest + "` field. Alternatively you may provide the full path as ") + "a string in <Link to=\"...\"> and the router will parse it for you.";
- }
- /**
- * @private
- *
- * When processing relative navigation we want to ignore ancestor routes that
- * do not contribute to the path, such that index/pathless layout routes don't
- * interfere.
- *
- * For example, when moving a route element into an index route and/or a
- * pathless layout route, relative link behavior contained within should stay
- * the same. Both of the following examples should link back to the root:
- *
- * <Route path="/">
- * <Route path="accounts" element={<Link to=".."}>
- * </Route>
- *
- * <Route path="/">
- * <Route path="accounts">
- * <Route element={<AccountsLayout />}> // <-- Does not contribute
- * <Route index element={<Link to=".."} /> // <-- Does not contribute
- * </Route
- * </Route>
- * </Route>
- */
- function getPathContributingMatches(matches) {
- return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
- }
- // Return the array of pathnames for the current route matches - used to
- // generate the routePathnames input for resolveTo()
- function getResolveToMatches(matches, v7_relativeSplatPath) {
- let pathMatches = getPathContributingMatches(matches);
- // When v7_relativeSplatPath is enabled, use the full pathname for the leaf
- // match so we include splat values for "." links. See:
- // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329
- if (v7_relativeSplatPath) {
- return pathMatches.map((match, idx) => idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase);
- }
- return pathMatches.map(match => match.pathnameBase);
- }
- /**
- * @private
- */
- function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {
- if (isPathRelative === void 0) {
- isPathRelative = false;
- }
- let to;
- if (typeof toArg === "string") {
- to = parsePath(toArg);
- } else {
- to = _extends({}, toArg);
- invariant(!to.pathname || !to.pathname.includes("?"), getInvalidPathError("?", "pathname", "search", to));
- invariant(!to.pathname || !to.pathname.includes("#"), getInvalidPathError("#", "pathname", "hash", to));
- invariant(!to.search || !to.search.includes("#"), getInvalidPathError("#", "search", "hash", to));
- }
- let isEmptyPath = toArg === "" || to.pathname === "";
- let toPathname = isEmptyPath ? "/" : to.pathname;
- let from;
- // Routing is relative to the current pathname if explicitly requested.
- //
- // If a pathname is explicitly provided in `to`, it should be relative to the
- // route context. This is explained in `Note on `<Link to>` values` in our
- // migration guide from v5 as a means of disambiguation between `to` values
- // that begin with `/` and those that do not. However, this is problematic for
- // `to` values that do not provide a pathname. `to` can simply be a search or
- // hash string, in which case we should assume that the navigation is relative
- // to the current location's pathname and *not* the route pathname.
- if (toPathname == null) {
- from = locationPathname;
- } else {
- let routePathnameIndex = routePathnames.length - 1;
- // With relative="route" (the default), each leading .. segment means
- // "go up one route" instead of "go up one URL segment". This is a key
- // difference from how <a href> works and a major reason we call this a
- // "to" value instead of a "href".
- if (!isPathRelative && toPathname.startsWith("..")) {
- let toSegments = toPathname.split("/");
- while (toSegments[0] === "..") {
- toSegments.shift();
- routePathnameIndex -= 1;
- }
- to.pathname = toSegments.join("/");
- }
- from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
- }
- let path = resolvePath(to, from);
- // Ensure the pathname has a trailing slash if the original "to" had one
- let hasExplicitTrailingSlash = toPathname && toPathname !== "/" && toPathname.endsWith("/");
- // Or if this was a link to the current path which has a trailing slash
- let hasCurrentTrailingSlash = (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
- if (!path.pathname.endsWith("/") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) {
- path.pathname += "/";
- }
- return path;
- }
- /**
- * @private
- */
- function getToPathname(to) {
- // Empty strings should be treated the same as / paths
- return to === "" || to.pathname === "" ? "/" : typeof to === "string" ? parsePath(to).pathname : to.pathname;
- }
- /**
- * @private
- */
- const joinPaths = paths => paths.join("/").replace(/\/\/+/g, "/");
- /**
- * @private
- */
- const normalizePathname = pathname => pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
- /**
- * @private
- */
- const normalizeSearch = search => !search || search === "?" ? "" : search.startsWith("?") ? search : "?" + search;
- /**
- * @private
- */
- const normalizeHash = hash => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
- /**
- * This is a shortcut for creating `application/json` responses. Converts `data`
- * to JSON and sets the `Content-Type` header.
- *
- * @deprecated The `json` method is deprecated in favor of returning raw objects.
- * This method will be removed in v7.
- */
- const json = function json(data, init) {
- if (init === void 0) {
- init = {};
- }
- let responseInit = typeof init === "number" ? {
- status: init
- } : init;
- let headers = new Headers(responseInit.headers);
- if (!headers.has("Content-Type")) {
- headers.set("Content-Type", "application/json; charset=utf-8");
- }
- return new Response(JSON.stringify(data), _extends({}, responseInit, {
- headers
- }));
- };
- class DataWithResponseInit {
- constructor(data, init) {
- this.type = "DataWithResponseInit";
- this.data = data;
- this.init = init || null;
- }
- }
- /**
- * Create "responses" that contain `status`/`headers` without forcing
- * serialization into an actual `Response` - used by Remix single fetch
- */
- function data(data, init) {
- return new DataWithResponseInit(data, typeof init === "number" ? {
- status: init
- } : init);
- }
- class AbortedDeferredError extends Error {}
- class DeferredData {
- constructor(data, responseInit) {
- this.pendingKeysSet = new Set();
- this.subscribers = new Set();
- this.deferredKeys = [];
- invariant(data && typeof data === "object" && !Array.isArray(data), "defer() only accepts plain objects");
- // Set up an AbortController + Promise we can race against to exit early
- // cancellation
- let reject;
- this.abortPromise = new Promise((_, r) => reject = r);
- this.controller = new AbortController();
- let onAbort = () => reject(new AbortedDeferredError("Deferred data aborted"));
- this.unlistenAbortSignal = () => this.controller.signal.removeEventListener("abort", onAbort);
- this.controller.signal.addEventListener("abort", onAbort);
- this.data = Object.entries(data).reduce((acc, _ref2) => {
- let [key, value] = _ref2;
- return Object.assign(acc, {
- [key]: this.trackPromise(key, value)
- });
- }, {});
- if (this.done) {
- // All incoming values were resolved
- this.unlistenAbortSignal();
- }
- this.init = responseInit;
- }
- trackPromise(key, value) {
- if (!(value instanceof Promise)) {
- return value;
- }
- this.deferredKeys.push(key);
- this.pendingKeysSet.add(key);
- // We store a little wrapper promise that will be extended with
- // _data/_error props upon resolve/reject
- let promise = Promise.race([value, this.abortPromise]).then(data => this.onSettle(promise, key, undefined, data), error => this.onSettle(promise, key, error));
- // Register rejection listeners to avoid uncaught promise rejections on
- // errors or aborted deferred values
- promise.catch(() => {});
- Object.defineProperty(promise, "_tracked", {
- get: () => true
- });
- return promise;
- }
- onSettle(promise, key, error, data) {
- if (this.controller.signal.aborted && error instanceof AbortedDeferredError) {
- this.unlistenAbortSignal();
- Object.defineProperty(promise, "_error", {
- get: () => error
- });
- return Promise.reject(error);
- }
- this.pendingKeysSet.delete(key);
- if (this.done) {
- // Nothing left to abort!
- this.unlistenAbortSignal();
- }
- // If the promise was resolved/rejected with undefined, we'll throw an error as you
- // should always resolve with a value or null
- if (error === undefined && data === undefined) {
- let undefinedError = new Error("Deferred data for key \"" + key + "\" resolved/rejected with `undefined`, " + "you must resolve/reject with a value or `null`.");
- Object.defineProperty(promise, "_error", {
- get: () => undefinedError
- });
- this.emit(false, key);
- return Promise.reject(undefinedError);
- }
- if (data === undefined) {
- Object.defineProperty(promise, "_error", {
- get: () => error
- });
- this.emit(false, key);
- return Promise.reject(error);
- }
- Object.defineProperty(promise, "_data", {
- get: () => data
- });
- this.emit(false, key);
- return data;
- }
- emit(aborted, settledKey) {
- this.subscribers.forEach(subscriber => subscriber(aborted, settledKey));
- }
- subscribe(fn) {
- this.subscribers.add(fn);
- return () => this.subscribers.delete(fn);
- }
- cancel() {
- this.controller.abort();
- this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));
- this.emit(true);
- }
- async resolveData(signal) {
- let aborted = false;
- if (!this.done) {
- let onAbort = () => this.cancel();
- signal.addEventListener("abort", onAbort);
- aborted = await new Promise(resolve => {
- this.subscribe(aborted => {
- signal.removeEventListener("abort", onAbort);
- if (aborted || this.done) {
- resolve(aborted);
- }
- });
- });
- }
- return aborted;
- }
- get done() {
- return this.pendingKeysSet.size === 0;
- }
- get unwrappedData() {
- invariant(this.data !== null && this.done, "Can only unwrap data on initialized and settled deferreds");
- return Object.entries(this.data).reduce((acc, _ref3) => {
- let [key, value] = _ref3;
- return Object.assign(acc, {
- [key]: unwrapTrackedPromise(value)
- });
- }, {});
- }
- get pendingKeys() {
- return Array.from(this.pendingKeysSet);
- }
- }
- function isTrackedPromise(value) {
- return value instanceof Promise && value._tracked === true;
- }
- function unwrapTrackedPromise(value) {
- if (!isTrackedPromise(value)) {
- return value;
- }
- if (value._error) {
- throw value._error;
- }
- return value._data;
- }
- /**
- * @deprecated The `defer` method is deprecated in favor of returning raw
- * objects. This method will be removed in v7.
- */
- const defer = function defer(data, init) {
- if (init === void 0) {
- init = {};
- }
- let responseInit = typeof init === "number" ? {
- status: init
- } : init;
- return new DeferredData(data, responseInit);
- };
- /**
- * A redirect response. Sets the status code and the `Location` header.
- * Defaults to "302 Found".
- */
- const redirect = function redirect(url, init) {
- if (init === void 0) {
- init = 302;
- }
- let responseInit = init;
- if (typeof responseInit === "number") {
- responseInit = {
- status: responseInit
- };
- } else if (typeof responseInit.status === "undefined") {
- responseInit.status = 302;
- }
- let headers = new Headers(responseInit.headers);
- headers.set("Location", url);
- return new Response(null, _extends({}, responseInit, {
- headers
- }));
- };
- /**
- * A redirect response that will force a document reload to the new location.
- * Sets the status code and the `Location` header.
- * Defaults to "302 Found".
- */
- const redirectDocument = (url, init) => {
- let response = redirect(url, init);
- response.headers.set("X-Remix-Reload-Document", "true");
- return response;
- };
- /**
- * A redirect response that will perform a `history.replaceState` instead of a
- * `history.pushState` for client-side navigation redirects.
- * Sets the status code and the `Location` header.
- * Defaults to "302 Found".
- */
- const replace = (url, init) => {
- let response = redirect(url, init);
- response.headers.set("X-Remix-Replace", "true");
- return response;
- };
- /**
- * @private
- * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
- *
- * We don't export the class for public use since it's an implementation
- * detail, but we export the interface above so folks can build their own
- * abstractions around instances via isRouteErrorResponse()
- */
- class ErrorResponseImpl {
- constructor(status, statusText, data, internal) {
- if (internal === void 0) {
- internal = false;
- }
- this.status = status;
- this.statusText = statusText || "";
- this.internal = internal;
- if (data instanceof Error) {
- this.data = data.toString();
- this.error = data;
- } else {
- this.data = data;
- }
- }
- }
- /**
- * Check if the given error is an ErrorResponse generated from a 4xx/5xx
- * Response thrown from an action/loader
- */
- function isRouteErrorResponse(error) {
- return error != null && typeof error.status === "number" && typeof error.statusText === "string" && typeof error.internal === "boolean" && "data" in error;
- }
- const validMutationMethodsArr = ["post", "put", "patch", "delete"];
- const validMutationMethods = new Set(validMutationMethodsArr);
- const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
- const validRequestMethods = new Set(validRequestMethodsArr);
- const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
- const redirectPreserveMethodStatusCodes = new Set([307, 308]);
- const IDLE_NAVIGATION = {
- state: "idle",
- location: undefined,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- json: undefined,
- text: undefined
- };
- const IDLE_FETCHER = {
- state: "idle",
- data: undefined,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- json: undefined,
- text: undefined
- };
- const IDLE_BLOCKER = {
- state: "unblocked",
- proceed: undefined,
- reset: undefined,
- location: undefined
- };
- const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
- const defaultMapRouteProperties = route => ({
- hasErrorBoundary: Boolean(route.hasErrorBoundary)
- });
- const TRANSITIONS_STORAGE_KEY = "remix-router-transitions";
- //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region createRouter
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * Create a router and listen to history POP navigations
- */
- function createRouter(init) {
- const routerWindow = init.window ? init.window : typeof window !== "undefined" ? window : undefined;
- const isBrowser = typeof routerWindow !== "undefined" && typeof routerWindow.document !== "undefined" && typeof routerWindow.document.createElement !== "undefined";
- const isServer = !isBrowser;
- invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter");
- let mapRouteProperties;
- if (init.mapRouteProperties) {
- mapRouteProperties = init.mapRouteProperties;
- } else if (init.detectErrorBoundary) {
- // If they are still using the deprecated version, wrap it with the new API
- let detectErrorBoundary = init.detectErrorBoundary;
- mapRouteProperties = route => ({
- hasErrorBoundary: detectErrorBoundary(route)
- });
- } else {
- mapRouteProperties = defaultMapRouteProperties;
- }
- // Routes keyed by ID
- let manifest = {};
- // Routes in tree format for matching
- let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
- let inFlightDataRoutes;
- let basename = init.basename || "/";
- let dataStrategyImpl = init.dataStrategy || defaultDataStrategy;
- let patchRoutesOnNavigationImpl = init.patchRoutesOnNavigation;
- // Config driven behavior flags
- let future = _extends({
- v7_fetcherPersist: false,
- v7_normalizeFormMethod: false,
- v7_partialHydration: false,
- v7_prependBasename: false,
- v7_relativeSplatPath: false,
- v7_skipActionErrorRevalidation: false
- }, init.future);
- // Cleanup function for history
- let unlistenHistory = null;
- // Externally-provided functions to call on all state changes
- let subscribers = new Set();
- // Externally-provided object to hold scroll restoration locations during routing
- let savedScrollPositions = null;
- // Externally-provided function to get scroll restoration keys
- let getScrollRestorationKey = null;
- // Externally-provided function to get current scroll position
- let getScrollPosition = null;
- // One-time flag to control the initial hydration scroll restoration. Because
- // we don't get the saved positions from <ScrollRestoration /> until _after_
- // the initial render, we need to manually trigger a separate updateState to
- // send along the restoreScrollPosition
- // Set to true if we have `hydrationData` since we assume we were SSR'd and that
- // SSR did the initial scroll restoration.
- let initialScrollRestored = init.hydrationData != null;
- let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
- let initialMatchesIsFOW = false;
- let initialErrors = null;
- if (initialMatches == null && !patchRoutesOnNavigationImpl) {
- // If we do not match a user-provided-route, fall back to the root
- // to allow the error boundary to take over
- let error = getInternalRouterError(404, {
- pathname: init.history.location.pathname
- });
- let {
- matches,
- route
- } = getShortCircuitMatches(dataRoutes);
- initialMatches = matches;
- initialErrors = {
- [route.id]: error
- };
- }
- // In SPA apps, if the user provided a patchRoutesOnNavigation implementation and
- // our initial match is a splat route, clear them out so we run through lazy
- // discovery on hydration in case there's a more accurate lazy route match.
- // In SSR apps (with `hydrationData`), we expect that the server will send
- // up the proper matched routes so we don't want to run lazy discovery on
- // initial hydration and want to hydrate into the splat route.
- if (initialMatches && !init.hydrationData) {
- let fogOfWar = checkFogOfWar(initialMatches, dataRoutes, init.history.location.pathname);
- if (fogOfWar.active) {
- initialMatches = null;
- }
- }
- let initialized;
- if (!initialMatches) {
- initialized = false;
- initialMatches = [];
- // If partial hydration and fog of war is enabled, we will be running
- // `patchRoutesOnNavigation` during hydration so include any partial matches as
- // the initial matches so we can properly render `HydrateFallback`'s
- if (future.v7_partialHydration) {
- let fogOfWar = checkFogOfWar(null, dataRoutes, init.history.location.pathname);
- if (fogOfWar.active && fogOfWar.matches) {
- initialMatchesIsFOW = true;
- initialMatches = fogOfWar.matches;
- }
- }
- } else if (initialMatches.some(m => m.route.lazy)) {
- // All initialMatches need to be loaded before we're ready. If we have lazy
- // functions around still then we'll need to run them in initialize()
- initialized = false;
- } else if (!initialMatches.some(m => m.route.loader)) {
- // If we've got no loaders to run, then we're good to go
- initialized = true;
- } else if (future.v7_partialHydration) {
- // If partial hydration is enabled, we're initialized so long as we were
- // provided with hydrationData for every route with a loader, and no loaders
- // were marked for explicit hydration
- let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
- let errors = init.hydrationData ? init.hydrationData.errors : null;
- // If errors exist, don't consider routes below the boundary
- if (errors) {
- let idx = initialMatches.findIndex(m => errors[m.route.id] !== undefined);
- initialized = initialMatches.slice(0, idx + 1).every(m => !shouldLoadRouteOnHydration(m.route, loaderData, errors));
- } else {
- initialized = initialMatches.every(m => !shouldLoadRouteOnHydration(m.route, loaderData, errors));
- }
- } else {
- // Without partial hydration - we're initialized if we were provided any
- // hydrationData - which is expected to be complete
- initialized = init.hydrationData != null;
- }
- let router;
- let state = {
- historyAction: init.history.action,
- location: init.history.location,
- matches: initialMatches,
- initialized,
- navigation: IDLE_NAVIGATION,
- // Don't restore on initial updateState() if we were SSR'd
- restoreScrollPosition: init.hydrationData != null ? false : null,
- preventScrollReset: false,
- revalidation: "idle",
- loaderData: init.hydrationData && init.hydrationData.loaderData || {},
- actionData: init.hydrationData && init.hydrationData.actionData || null,
- errors: init.hydrationData && init.hydrationData.errors || initialErrors,
- fetchers: new Map(),
- blockers: new Map()
- };
- // -- Stateful internal variables to manage navigations --
- // Current navigation in progress (to be committed in completeNavigation)
- let pendingAction = Action.Pop;
- // Should the current navigation prevent the scroll reset if scroll cannot
- // be restored?
- let pendingPreventScrollReset = false;
- // AbortController for the active navigation
- let pendingNavigationController;
- // Should the current navigation enable document.startViewTransition?
- let pendingViewTransitionEnabled = false;
- // Store applied view transitions so we can apply them on POP
- let appliedViewTransitions = new Map();
- // Cleanup function for persisting applied transitions to sessionStorage
- let removePageHideEventListener = null;
- // We use this to avoid touching history in completeNavigation if a
- // revalidation is entirely uninterrupted
- let isUninterruptedRevalidation = false;
- // Use this internal flag to force revalidation of all loaders:
- // - submissions (completed or interrupted)
- // - useRevalidator()
- // - X-Remix-Revalidate (from redirect)
- let isRevalidationRequired = false;
- // Use this internal array to capture routes that require revalidation due
- // to a cancelled deferred on action submission
- let cancelledDeferredRoutes = [];
- // Use this internal array to capture fetcher loads that were cancelled by an
- // action navigation and require revalidation
- let cancelledFetcherLoads = new Set();
- // AbortControllers for any in-flight fetchers
- let fetchControllers = new Map();
- // Track loads based on the order in which they started
- let incrementingLoadId = 0;
- // Track the outstanding pending navigation data load to be compared against
- // the globally incrementing load when a fetcher load lands after a completed
- // navigation
- let pendingNavigationLoadId = -1;
- // Fetchers that triggered data reloads as a result of their actions
- let fetchReloadIds = new Map();
- // Fetchers that triggered redirect navigations
- let fetchRedirectIds = new Set();
- // Most recent href/match for fetcher.load calls for fetchers
- let fetchLoadMatches = new Map();
- // Ref-count mounted fetchers so we know when it's ok to clean them up
- let activeFetchers = new Map();
- // Fetchers that have requested a delete when using v7_fetcherPersist,
- // they'll be officially removed after they return to idle
- let deletedFetchers = new Set();
- // Store DeferredData instances for active route matches. When a
- // route loader returns defer() we stick one in here. Then, when a nested
- // promise resolves we update loaderData. If a new navigation starts we
- // cancel active deferreds for eliminated routes.
- let activeDeferreds = new Map();
- // Store blocker functions in a separate Map outside of router state since
- // we don't need to update UI state if they change
- let blockerFunctions = new Map();
- // Flag to ignore the next history update, so we can revert the URL change on
- // a POP navigation that was blocked by the user without touching router state
- let unblockBlockerHistoryUpdate = undefined;
- // Initialize the router, all side effects should be kicked off from here.
- // Implemented as a Fluent API for ease of:
- // let router = createRouter(init).initialize();
- function initialize() {
- // If history informs us of a POP navigation, start the navigation but do not update
- // state. We'll update our own state once the navigation completes
- unlistenHistory = init.history.listen(_ref => {
- let {
- action: historyAction,
- location,
- delta
- } = _ref;
- // Ignore this event if it was just us resetting the URL from a
- // blocked POP navigation
- if (unblockBlockerHistoryUpdate) {
- unblockBlockerHistoryUpdate();
- unblockBlockerHistoryUpdate = undefined;
- return;
- }
- warning(blockerFunctions.size === 0 || delta != null, "You are trying to use a blocker on a POP navigation to a location " + "that was not created by @remix-run/router. This will fail silently in " + "production. This can happen if you are navigating outside the router " + "via `window.history.pushState`/`window.location.hash` instead of using " + "router navigation APIs. This can also happen if you are using " + "createHashRouter and the user manually changes the URL.");
- let blockerKey = shouldBlockNavigation({
- currentLocation: state.location,
- nextLocation: location,
- historyAction
- });
- if (blockerKey && delta != null) {
- // Restore the URL to match the current UI, but don't update router state
- let nextHistoryUpdatePromise = new Promise(resolve => {
- unblockBlockerHistoryUpdate = resolve;
- });
- init.history.go(delta * -1);
- // Put the blocker into a blocked state
- updateBlocker(blockerKey, {
- state: "blocked",
- location,
- proceed() {
- updateBlocker(blockerKey, {
- state: "proceeding",
- proceed: undefined,
- reset: undefined,
- location
- });
- // Re-do the same POP navigation we just blocked, after the url
- // restoration is also complete. See:
- // https://github.com/remix-run/react-router/issues/11613
- nextHistoryUpdatePromise.then(() => init.history.go(delta));
- },
- reset() {
- let blockers = new Map(state.blockers);
- blockers.set(blockerKey, IDLE_BLOCKER);
- updateState({
- blockers
- });
- }
- });
- return;
- }
- return startNavigation(historyAction, location);
- });
- if (isBrowser) {
- // FIXME: This feels gross. How can we cleanup the lines between
- // scrollRestoration/appliedTransitions persistance?
- restoreAppliedTransitions(routerWindow, appliedViewTransitions);
- let _saveAppliedTransitions = () => persistAppliedTransitions(routerWindow, appliedViewTransitions);
- routerWindow.addEventListener("pagehide", _saveAppliedTransitions);
- removePageHideEventListener = () => routerWindow.removeEventListener("pagehide", _saveAppliedTransitions);
- }
- // Kick off initial data load if needed. Use Pop to avoid modifying history
- // Note we don't do any handling of lazy here. For SPA's it'll get handled
- // in the normal navigation flow. For SSR it's expected that lazy modules are
- // resolved prior to router creation since we can't go into a fallbackElement
- // UI for SSR'd apps
- if (!state.initialized) {
- startNavigation(Action.Pop, state.location, {
- initialHydration: true
- });
- }
- return router;
- }
- // Clean up a router and it's side effects
- function dispose() {
- if (unlistenHistory) {
- unlistenHistory();
- }
- if (removePageHideEventListener) {
- removePageHideEventListener();
- }
- subscribers.clear();
- pendingNavigationController && pendingNavigationController.abort();
- state.fetchers.forEach((_, key) => deleteFetcher(key));
- state.blockers.forEach((_, key) => deleteBlocker(key));
- }
- // Subscribe to state updates for the router
- function subscribe(fn) {
- subscribers.add(fn);
- return () => subscribers.delete(fn);
- }
- // Update our state and notify the calling context of the change
- function updateState(newState, opts) {
- if (opts === void 0) {
- opts = {};
- }
- state = _extends({}, state, newState);
- // Prep fetcher cleanup so we can tell the UI which fetcher data entries
- // can be removed
- let completedFetchers = [];
- let deletedFetchersKeys = [];
- if (future.v7_fetcherPersist) {
- state.fetchers.forEach((fetcher, key) => {
- if (fetcher.state === "idle") {
- if (deletedFetchers.has(key)) {
- // Unmounted from the UI and can be totally removed
- deletedFetchersKeys.push(key);
- } else {
- // Returned to idle but still mounted in the UI, so semi-remains for
- // revalidations and such
- completedFetchers.push(key);
- }
- }
- });
- }
- // Remove any lingering deleted fetchers that have already been removed
- // from state.fetchers
- deletedFetchers.forEach(key => {
- if (!state.fetchers.has(key) && !fetchControllers.has(key)) {
- deletedFetchersKeys.push(key);
- }
- });
- // Iterate over a local copy so that if flushSync is used and we end up
- // removing and adding a new subscriber due to the useCallback dependencies,
- // we don't get ourselves into a loop calling the new subscriber immediately
- [...subscribers].forEach(subscriber => subscriber(state, {
- deletedFetchers: deletedFetchersKeys,
- viewTransitionOpts: opts.viewTransitionOpts,
- flushSync: opts.flushSync === true
- }));
- // Remove idle fetchers from state since we only care about in-flight fetchers.
- if (future.v7_fetcherPersist) {
- completedFetchers.forEach(key => state.fetchers.delete(key));
- deletedFetchersKeys.forEach(key => deleteFetcher(key));
- } else {
- // We already called deleteFetcher() on these, can remove them from this
- // Set now that we've handed the keys off to the data layer
- deletedFetchersKeys.forEach(key => deletedFetchers.delete(key));
- }
- }
- // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
- // and setting state.[historyAction/location/matches] to the new route.
- // - Location is a required param
- // - Navigation will always be set to IDLE_NAVIGATION
- // - Can pass any other state in newState
- function completeNavigation(location, newState, _temp) {
- var _location$state, _location$state2;
- let {
- flushSync
- } = _temp === void 0 ? {} : _temp;
- // Deduce if we're in a loading/actionReload state:
- // - We have committed actionData in the store
- // - The current navigation was a mutation submission
- // - We're past the submitting state and into the loading state
- // - The location being loaded is not the result of a redirect
- let isActionReload = state.actionData != null && state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && state.navigation.state === "loading" && ((_location$state = location.state) == null ? void 0 : _location$state._isRedirect) !== true;
- let actionData;
- if (newState.actionData) {
- if (Object.keys(newState.actionData).length > 0) {
- actionData = newState.actionData;
- } else {
- // Empty actionData -> clear prior actionData due to an action error
- actionData = null;
- }
- } else if (isActionReload) {
- // Keep the current data if we're wrapping up the action reload
- actionData = state.actionData;
- } else {
- // Clear actionData on any other completed navigations
- actionData = null;
- }
- // Always preserve any existing loaderData from re-used routes
- let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData;
- // On a successful navigation we can assume we got through all blockers
- // so we can start fresh
- let blockers = state.blockers;
- if (blockers.size > 0) {
- blockers = new Map(blockers);
- blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER));
- }
- // Always respect the user flag. Otherwise don't reset on mutation
- // submission navigations unless they redirect
- let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;
- // Commit any in-flight routes at the end of the HMR revalidation "navigation"
- if (inFlightDataRoutes) {
- dataRoutes = inFlightDataRoutes;
- inFlightDataRoutes = undefined;
- }
- if (isUninterruptedRevalidation) ; else if (pendingAction === Action.Pop) ; else if (pendingAction === Action.Push) {
- init.history.push(location, location.state);
- } else if (pendingAction === Action.Replace) {
- init.history.replace(location, location.state);
- }
- let viewTransitionOpts;
- // On POP, enable transitions if they were enabled on the original navigation
- if (pendingAction === Action.Pop) {
- // Forward takes precedence so they behave like the original navigation
- let priorPaths = appliedViewTransitions.get(state.location.pathname);
- if (priorPaths && priorPaths.has(location.pathname)) {
- viewTransitionOpts = {
- currentLocation: state.location,
- nextLocation: location
- };
- } else if (appliedViewTransitions.has(location.pathname)) {
- // If we don't have a previous forward nav, assume we're popping back to
- // the new location and enable if that location previously enabled
- viewTransitionOpts = {
- currentLocation: location,
- nextLocation: state.location
- };
- }
- } else if (pendingViewTransitionEnabled) {
- // Store the applied transition on PUSH/REPLACE
- let toPaths = appliedViewTransitions.get(state.location.pathname);
- if (toPaths) {
- toPaths.add(location.pathname);
- } else {
- toPaths = new Set([location.pathname]);
- appliedViewTransitions.set(state.location.pathname, toPaths);
- }
- viewTransitionOpts = {
- currentLocation: state.location,
- nextLocation: location
- };
- }
- updateState(_extends({}, newState, {
- actionData,
- loaderData,
- historyAction: pendingAction,
- location,
- initialized: true,
- navigation: IDLE_NAVIGATION,
- revalidation: "idle",
- restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
- preventScrollReset,
- blockers
- }), {
- viewTransitionOpts,
- flushSync: flushSync === true
- });
- // Reset stateful navigation vars
- pendingAction = Action.Pop;
- pendingPreventScrollReset = false;
- pendingViewTransitionEnabled = false;
- isUninterruptedRevalidation = false;
- isRevalidationRequired = false;
- cancelledDeferredRoutes = [];
- }
- // Trigger a navigation event, which can either be a numerical POP or a PUSH
- // replace with an optional submission
- async function navigate(to, opts) {
- if (typeof to === "number") {
- init.history.go(to);
- return;
- }
- let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, future.v7_relativeSplatPath, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
- let {
- path,
- submission,
- error
- } = normalizeNavigateOptions(future.v7_normalizeFormMethod, false, normalizedPath, opts);
- let currentLocation = state.location;
- let nextLocation = createLocation(state.location, path, opts && opts.state);
- // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
- // URL from window.location, so we need to encode it here so the behavior
- // remains the same as POP and non-data-router usages. new URL() does all
- // the same encoding we'd get from a history.pushState/window.location read
- // without having to touch history
- nextLocation = _extends({}, nextLocation, init.history.encodeLocation(nextLocation));
- let userReplace = opts && opts.replace != null ? opts.replace : undefined;
- let historyAction = Action.Push;
- if (userReplace === true) {
- historyAction = Action.Replace;
- } else if (userReplace === false) ; else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) {
- // By default on submissions to the current location we REPLACE so that
- // users don't have to double-click the back button to get to the prior
- // location. If the user redirects to a different location from the
- // action/loader this will be ignored and the redirect will be a PUSH
- historyAction = Action.Replace;
- }
- let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
- let flushSync = (opts && opts.flushSync) === true;
- let blockerKey = shouldBlockNavigation({
- currentLocation,
- nextLocation,
- historyAction
- });
- if (blockerKey) {
- // Put the blocker into a blocked state
- updateBlocker(blockerKey, {
- state: "blocked",
- location: nextLocation,
- proceed() {
- updateBlocker(blockerKey, {
- state: "proceeding",
- proceed: undefined,
- reset: undefined,
- location: nextLocation
- });
- // Send the same navigation through
- navigate(to, opts);
- },
- reset() {
- let blockers = new Map(state.blockers);
- blockers.set(blockerKey, IDLE_BLOCKER);
- updateState({
- blockers
- });
- }
- });
- return;
- }
- return await startNavigation(historyAction, nextLocation, {
- submission,
- // Send through the formData serialization error if we have one so we can
- // render at the right error boundary after we match routes
- pendingError: error,
- preventScrollReset,
- replace: opts && opts.replace,
- enableViewTransition: opts && opts.viewTransition,
- flushSync
- });
- }
- // Revalidate all current loaders. If a navigation is in progress or if this
- // is interrupted by a navigation, allow this to "succeed" by calling all
- // loaders during the next loader round
- function revalidate() {
- interruptActiveLoads();
- updateState({
- revalidation: "loading"
- });
- // If we're currently submitting an action, we don't need to start a new
- // navigation, we'll just let the follow up loader execution call all loaders
- if (state.navigation.state === "submitting") {
- return;
- }
- // If we're currently in an idle state, start a new navigation for the current
- // action/location and mark it as uninterrupted, which will skip the history
- // update in completeNavigation
- if (state.navigation.state === "idle") {
- startNavigation(state.historyAction, state.location, {
- startUninterruptedRevalidation: true
- });
- return;
- }
- // Otherwise, if we're currently in a loading state, just start a new
- // navigation to the navigation.location but do not trigger an uninterrupted
- // revalidation so that history correctly updates once the navigation completes
- startNavigation(pendingAction || state.historyAction, state.navigation.location, {
- overrideNavigation: state.navigation,
- // Proxy through any rending view transition
- enableViewTransition: pendingViewTransitionEnabled === true
- });
- }
- // Start a navigation to the given action/location. Can optionally provide a
- // overrideNavigation which will override the normalLoad in the case of a redirect
- // navigation
- async function startNavigation(historyAction, location, opts) {
- // Abort any in-progress navigations and start a new one. Unset any ongoing
- // uninterrupted revalidations unless told otherwise, since we want this
- // new navigation to update history normally
- pendingNavigationController && pendingNavigationController.abort();
- pendingNavigationController = null;
- pendingAction = historyAction;
- isUninterruptedRevalidation = (opts && opts.startUninterruptedRevalidation) === true;
- // Save the current scroll position every time we start a new navigation,
- // and track whether we should reset scroll on completion
- saveScrollPosition(state.location, state.matches);
- pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
- pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
- let routesToUse = inFlightDataRoutes || dataRoutes;
- let loadingNavigation = opts && opts.overrideNavigation;
- let matches = opts != null && opts.initialHydration && state.matches && state.matches.length > 0 && !initialMatchesIsFOW ?
- // `matchRoutes()` has already been called if we're in here via `router.initialize()`
- state.matches : matchRoutes(routesToUse, location, basename);
- let flushSync = (opts && opts.flushSync) === true;
- // Short circuit if it's only a hash change and not a revalidation or
- // mutation submission.
- //
- // Ignore on initial page loads because since the initial hydration will always
- // be "same hash". For example, on /page#hash and submit a <Form method="post">
- // which will default to a navigation to /page
- if (matches && state.initialized && !isRevalidationRequired && isHashChangeOnly(state.location, location) && !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))) {
- completeNavigation(location, {
- matches
- }, {
- flushSync
- });
- return;
- }
- let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);
- if (fogOfWar.active && fogOfWar.matches) {
- matches = fogOfWar.matches;
- }
- // Short circuit with a 404 on the root error boundary if we match nothing
- if (!matches) {
- let {
- error,
- notFoundMatches,
- route
- } = handleNavigational404(location.pathname);
- completeNavigation(location, {
- matches: notFoundMatches,
- loaderData: {},
- errors: {
- [route.id]: error
- }
- }, {
- flushSync
- });
- return;
- }
- // Create a controller/Request for this navigation
- pendingNavigationController = new AbortController();
- let request = createClientSideRequest(init.history, location, pendingNavigationController.signal, opts && opts.submission);
- let pendingActionResult;
- if (opts && opts.pendingError) {
- // If we have a pendingError, it means the user attempted a GET submission
- // with binary FormData so assign here and skip to handleLoaders. That
- // way we handle calling loaders above the boundary etc. It's not really
- // different from an actionError in that sense.
- pendingActionResult = [findNearestBoundary(matches).route.id, {
- type: ResultType.error,
- error: opts.pendingError
- }];
- } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
- // Call action if we received an action submission
- let actionResult = await handleAction(request, location, opts.submission, matches, fogOfWar.active, {
- replace: opts.replace,
- flushSync
- });
- if (actionResult.shortCircuited) {
- return;
- }
- // If we received a 404 from handleAction, it's because we couldn't lazily
- // discover the destination route so we don't want to call loaders
- if (actionResult.pendingActionResult) {
- let [routeId, result] = actionResult.pendingActionResult;
- if (isErrorResult(result) && isRouteErrorResponse(result.error) && result.error.status === 404) {
- pendingNavigationController = null;
- completeNavigation(location, {
- matches: actionResult.matches,
- loaderData: {},
- errors: {
- [routeId]: result.error
- }
- });
- return;
- }
- }
- matches = actionResult.matches || matches;
- pendingActionResult = actionResult.pendingActionResult;
- loadingNavigation = getLoadingNavigation(location, opts.submission);
- flushSync = false;
- // No need to do fog of war matching again on loader execution
- fogOfWar.active = false;
- // Create a GET request for the loaders
- request = createClientSideRequest(init.history, request.url, request.signal);
- }
- // Call loaders
- let {
- shortCircuited,
- matches: updatedMatches,
- loaderData,
- errors
- } = await handleLoaders(request, location, matches, fogOfWar.active, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionResult);
- if (shortCircuited) {
- return;
- }
- // Clean up now that the action/loaders have completed. Don't clean up if
- // we short circuited because pendingNavigationController will have already
- // been assigned to a new controller for the next navigation
- pendingNavigationController = null;
- completeNavigation(location, _extends({
- matches: updatedMatches || matches
- }, getActionDataForCommit(pendingActionResult), {
- loaderData,
- errors
- }));
- }
- // Call the action matched by the leaf route for this navigation and handle
- // redirects/errors
- async function handleAction(request, location, submission, matches, isFogOfWar, opts) {
- if (opts === void 0) {
- opts = {};
- }
- interruptActiveLoads();
- // Put us in a submitting state
- let navigation = getSubmittingNavigation(location, submission);
- updateState({
- navigation
- }, {
- flushSync: opts.flushSync === true
- });
- if (isFogOfWar) {
- let discoverResult = await discoverRoutes(matches, location.pathname, request.signal);
- if (discoverResult.type === "aborted") {
- return {
- shortCircuited: true
- };
- } else if (discoverResult.type === "error") {
- let boundaryId = findNearestBoundary(discoverResult.partialMatches).route.id;
- return {
- matches: discoverResult.partialMatches,
- pendingActionResult: [boundaryId, {
- type: ResultType.error,
- error: discoverResult.error
- }]
- };
- } else if (!discoverResult.matches) {
- let {
- notFoundMatches,
- error,
- route
- } = handleNavigational404(location.pathname);
- return {
- matches: notFoundMatches,
- pendingActionResult: [route.id, {
- type: ResultType.error,
- error
- }]
- };
- } else {
- matches = discoverResult.matches;
- }
- }
- // Call our action and get the result
- let result;
- let actionMatch = getTargetMatch(matches, location);
- if (!actionMatch.route.action && !actionMatch.route.lazy) {
- result = {
- type: ResultType.error,
- error: getInternalRouterError(405, {
- method: request.method,
- pathname: location.pathname,
- routeId: actionMatch.route.id
- })
- };
- } else {
- let results = await callDataStrategy("action", state, request, [actionMatch], matches, null);
- result = results[actionMatch.route.id];
- if (request.signal.aborted) {
- return {
- shortCircuited: true
- };
- }
- }
- if (isRedirectResult(result)) {
- let replace;
- if (opts && opts.replace != null) {
- replace = opts.replace;
- } else {
- // If the user didn't explicity indicate replace behavior, replace if
- // we redirected to the exact same location we're currently at to avoid
- // double back-buttons
- let location = normalizeRedirectLocation(result.response.headers.get("Location"), new URL(request.url), basename);
- replace = location === state.location.pathname + state.location.search;
- }
- await startRedirectNavigation(request, result, true, {
- submission,
- replace
- });
- return {
- shortCircuited: true
- };
- }
- if (isDeferredResult(result)) {
- throw getInternalRouterError(400, {
- type: "defer-action"
- });
- }
- if (isErrorResult(result)) {
- // Store off the pending error - we use it to determine which loaders
- // to call and will commit it when we complete the navigation
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
- // By default, all submissions to the current location are REPLACE
- // navigations, but if the action threw an error that'll be rendered in
- // an errorElement, we fall back to PUSH so that the user can use the
- // back button to get back to the pre-submission form location to try
- // again
- if ((opts && opts.replace) !== true) {
- pendingAction = Action.Push;
- }
- return {
- matches,
- pendingActionResult: [boundaryMatch.route.id, result]
- };
- }
- return {
- matches,
- pendingActionResult: [actionMatch.route.id, result]
- };
- }
- // Call all applicable loaders for the given matches, handling redirects,
- // errors, etc.
- async function handleLoaders(request, location, matches, isFogOfWar, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionResult) {
- // Figure out the right navigation we want to use for data loading
- let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
- // If this was a redirect from an action we don't have a "submission" but
- // we have it on the loading navigation so use that if available
- let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
- // If this is an uninterrupted revalidation, we remain in our current idle
- // state. If not, we need to switch to our loading state and load data,
- // preserving any new action data or existing action data (in the case of
- // a revalidation interrupting an actionReload)
- // If we have partialHydration enabled, then don't update the state for the
- // initial data load since it's not a "navigation"
- let shouldUpdateNavigationState = !isUninterruptedRevalidation && (!future.v7_partialHydration || !initialHydration);
- // When fog of war is enabled, we enter our `loading` state earlier so we
- // can discover new routes during the `loading` state. We skip this if
- // we've already run actions since we would have done our matching already.
- // If the children() function threw then, we want to proceed with the
- // partial matches it discovered.
- if (isFogOfWar) {
- if (shouldUpdateNavigationState) {
- let actionData = getUpdatedActionData(pendingActionResult);
- updateState(_extends({
- navigation: loadingNavigation
- }, actionData !== undefined ? {
- actionData
- } : {}), {
- flushSync
- });
- }
- let discoverResult = await discoverRoutes(matches, location.pathname, request.signal);
- if (discoverResult.type === "aborted") {
- return {
- shortCircuited: true
- };
- } else if (discoverResult.type === "error") {
- let boundaryId = findNearestBoundary(discoverResult.partialMatches).route.id;
- return {
- matches: discoverResult.partialMatches,
- loaderData: {},
- errors: {
- [boundaryId]: discoverResult.error
- }
- };
- } else if (!discoverResult.matches) {
- let {
- error,
- notFoundMatches,
- route
- } = handleNavigational404(location.pathname);
- return {
- matches: notFoundMatches,
- loaderData: {},
- errors: {
- [route.id]: error
- }
- };
- } else {
- matches = discoverResult.matches;
- }
- }
- let routesToUse = inFlightDataRoutes || dataRoutes;
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, future.v7_partialHydration && initialHydration === true, future.v7_skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult);
- // Cancel pending deferreds for no-longer-matched routes or routes we're
- // about to reload. Note that if this is an action reload we would have
- // already cancelled all pending deferreds so this would be a no-op
- cancelActiveDeferreds(routeId => !(matches && matches.some(m => m.route.id === routeId)) || matchesToLoad && matchesToLoad.some(m => m.route.id === routeId));
- pendingNavigationLoadId = ++incrementingLoadId;
- // Short circuit if we have no loaders to run
- if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
- let updatedFetchers = markFetchRedirectsDone();
- completeNavigation(location, _extends({
- matches,
- loaderData: {},
- // Commit pending error if we're short circuiting
- errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? {
- [pendingActionResult[0]]: pendingActionResult[1].error
- } : null
- }, getActionDataForCommit(pendingActionResult), updatedFetchers ? {
- fetchers: new Map(state.fetchers)
- } : {}), {
- flushSync
- });
- return {
- shortCircuited: true
- };
- }
- if (shouldUpdateNavigationState) {
- let updates = {};
- if (!isFogOfWar) {
- // Only update navigation/actionNData if we didn't already do it above
- updates.navigation = loadingNavigation;
- let actionData = getUpdatedActionData(pendingActionResult);
- if (actionData !== undefined) {
- updates.actionData = actionData;
- }
- }
- if (revalidatingFetchers.length > 0) {
- updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers);
- }
- updateState(updates, {
- flushSync
- });
- }
- revalidatingFetchers.forEach(rf => {
- abortFetcher(rf.key);
- if (rf.controller) {
- // Fetchers use an independent AbortController so that aborting a fetcher
- // (via deleteFetcher) does not abort the triggering navigation that
- // triggered the revalidation
- fetchControllers.set(rf.key, rf.controller);
- }
- });
- // Proxy navigation abort through to revalidation fetchers
- let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(f => abortFetcher(f.key));
- if (pendingNavigationController) {
- pendingNavigationController.signal.addEventListener("abort", abortPendingFetchRevalidations);
- }
- let {
- loaderResults,
- fetcherResults
- } = await callLoadersAndMaybeResolveData(state, matches, matchesToLoad, revalidatingFetchers, request);
- if (request.signal.aborted) {
- return {
- shortCircuited: true
- };
- }
- // Clean up _after_ loaders have completed. Don't clean up if we short
- // circuited because fetchControllers would have been aborted and
- // reassigned to new controllers for the next navigation
- if (pendingNavigationController) {
- pendingNavigationController.signal.removeEventListener("abort", abortPendingFetchRevalidations);
- }
- revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key));
- // If any loaders returned a redirect Response, start a new REPLACE navigation
- let redirect = findRedirect(loaderResults);
- if (redirect) {
- await startRedirectNavigation(request, redirect.result, true, {
- replace
- });
- return {
- shortCircuited: true
- };
- }
- redirect = findRedirect(fetcherResults);
- if (redirect) {
- // If this redirect came from a fetcher make sure we mark it in
- // fetchRedirectIds so it doesn't get revalidated on the next set of
- // loader executions
- fetchRedirectIds.add(redirect.key);
- await startRedirectNavigation(request, redirect.result, true, {
- replace
- });
- return {
- shortCircuited: true
- };
- }
- // Process and commit output from loaders
- let {
- loaderData,
- errors
- } = processLoaderData(state, matches, loaderResults, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds);
- // Wire up subscribers to update loaderData as promises settle
- activeDeferreds.forEach((deferredData, routeId) => {
- deferredData.subscribe(aborted => {
- // Note: No need to updateState here since the TrackedPromise on
- // loaderData is stable across resolve/reject
- // Remove this instance if we were aborted or if promises have settled
- if (aborted || deferredData.done) {
- activeDeferreds.delete(routeId);
- }
- });
- });
- // Preserve SSR errors during partial hydration
- if (future.v7_partialHydration && initialHydration && state.errors) {
- errors = _extends({}, state.errors, errors);
- }
- let updatedFetchers = markFetchRedirectsDone();
- let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
- let shouldUpdateFetchers = updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
- return _extends({
- matches,
- loaderData,
- errors
- }, shouldUpdateFetchers ? {
- fetchers: new Map(state.fetchers)
- } : {});
- }
- function getUpdatedActionData(pendingActionResult) {
- if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
- // This is cast to `any` currently because `RouteData`uses any and it
- // would be a breaking change to use any.
- // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
- return {
- [pendingActionResult[0]]: pendingActionResult[1].data
- };
- } else if (state.actionData) {
- if (Object.keys(state.actionData).length === 0) {
- return null;
- } else {
- return state.actionData;
- }
- }
- }
- function getUpdatedRevalidatingFetchers(revalidatingFetchers) {
- revalidatingFetchers.forEach(rf => {
- let fetcher = state.fetchers.get(rf.key);
- let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
- state.fetchers.set(rf.key, revalidatingFetcher);
- });
- return new Map(state.fetchers);
- }
- // Trigger a fetcher load/submit for the given fetcher key
- function fetch(key, routeId, href, opts) {
- if (isServer) {
- throw new Error("router.fetch() was called during the server render, but it shouldn't be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");
- }
- abortFetcher(key);
- let flushSync = (opts && opts.flushSync) === true;
- let routesToUse = inFlightDataRoutes || dataRoutes;
- let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, future.v7_relativeSplatPath, routeId, opts == null ? void 0 : opts.relative);
- let matches = matchRoutes(routesToUse, normalizedPath, basename);
- let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath);
- if (fogOfWar.active && fogOfWar.matches) {
- matches = fogOfWar.matches;
- }
- if (!matches) {
- setFetcherError(key, routeId, getInternalRouterError(404, {
- pathname: normalizedPath
- }), {
- flushSync
- });
- return;
- }
- let {
- path,
- submission,
- error
- } = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
- if (error) {
- setFetcherError(key, routeId, error, {
- flushSync
- });
- return;
- }
- let match = getTargetMatch(matches, path);
- let preventScrollReset = (opts && opts.preventScrollReset) === true;
- if (submission && isMutationMethod(submission.formMethod)) {
- handleFetcherAction(key, routeId, path, match, matches, fogOfWar.active, flushSync, preventScrollReset, submission);
- return;
- }
- // Store off the match so we can call it's shouldRevalidate on subsequent
- // revalidations
- fetchLoadMatches.set(key, {
- routeId,
- path
- });
- handleFetcherLoader(key, routeId, path, match, matches, fogOfWar.active, flushSync, preventScrollReset, submission);
- }
- // Call the action for the matched fetcher.submit(), and then handle redirects,
- // errors, and revalidation
- async function handleFetcherAction(key, routeId, path, match, requestMatches, isFogOfWar, flushSync, preventScrollReset, submission) {
- interruptActiveLoads();
- fetchLoadMatches.delete(key);
- function detectAndHandle405Error(m) {
- if (!m.route.action && !m.route.lazy) {
- let error = getInternalRouterError(405, {
- method: submission.formMethod,
- pathname: path,
- routeId: routeId
- });
- setFetcherError(key, routeId, error, {
- flushSync
- });
- return true;
- }
- return false;
- }
- if (!isFogOfWar && detectAndHandle405Error(match)) {
- return;
- }
- // Put this fetcher into it's submitting state
- let existingFetcher = state.fetchers.get(key);
- updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
- flushSync
- });
- let abortController = new AbortController();
- let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
- if (isFogOfWar) {
- let discoverResult = await discoverRoutes(requestMatches, new URL(fetchRequest.url).pathname, fetchRequest.signal, key);
- if (discoverResult.type === "aborted") {
- return;
- } else if (discoverResult.type === "error") {
- setFetcherError(key, routeId, discoverResult.error, {
- flushSync
- });
- return;
- } else if (!discoverResult.matches) {
- setFetcherError(key, routeId, getInternalRouterError(404, {
- pathname: path
- }), {
- flushSync
- });
- return;
- } else {
- requestMatches = discoverResult.matches;
- match = getTargetMatch(requestMatches, path);
- if (detectAndHandle405Error(match)) {
- return;
- }
- }
- }
- // Call the action for the fetcher
- fetchControllers.set(key, abortController);
- let originatingLoadId = incrementingLoadId;
- let actionResults = await callDataStrategy("action", state, fetchRequest, [match], requestMatches, key);
- let actionResult = actionResults[match.route.id];
- if (fetchRequest.signal.aborted) {
- // We can delete this so long as we weren't aborted by our own fetcher
- // re-submit which would have put _new_ controller is in fetchControllers
- if (fetchControllers.get(key) === abortController) {
- fetchControllers.delete(key);
- }
- return;
- }
- // When using v7_fetcherPersist, we don't want errors bubbling up to the UI
- // or redirects processed for unmounted fetchers so we just revert them to
- // idle
- if (future.v7_fetcherPersist && deletedFetchers.has(key)) {
- if (isRedirectResult(actionResult) || isErrorResult(actionResult)) {
- updateFetcherState(key, getDoneFetcher(undefined));
- return;
- }
- // Let SuccessResult's fall through for revalidation
- } else {
- if (isRedirectResult(actionResult)) {
- fetchControllers.delete(key);
- if (pendingNavigationLoadId > originatingLoadId) {
- // A new navigation was kicked off after our action started, so that
- // should take precedence over this redirect navigation. We already
- // set isRevalidationRequired so all loaders for the new route should
- // fire unless opted out via shouldRevalidate
- updateFetcherState(key, getDoneFetcher(undefined));
- return;
- } else {
- fetchRedirectIds.add(key);
- updateFetcherState(key, getLoadingFetcher(submission));
- return startRedirectNavigation(fetchRequest, actionResult, false, {
- fetcherSubmission: submission,
- preventScrollReset
- });
- }
- }
- // Process any non-redirect errors thrown
- if (isErrorResult(actionResult)) {
- setFetcherError(key, routeId, actionResult.error);
- return;
- }
- }
- if (isDeferredResult(actionResult)) {
- throw getInternalRouterError(400, {
- type: "defer-action"
- });
- }
- // Start the data load for current matches, or the next location if we're
- // in the middle of a navigation
- let nextLocation = state.navigation.location || state.location;
- let revalidationRequest = createClientSideRequest(init.history, nextLocation, abortController.signal);
- let routesToUse = inFlightDataRoutes || dataRoutes;
- let matches = state.navigation.state !== "idle" ? matchRoutes(routesToUse, state.navigation.location, basename) : state.matches;
- invariant(matches, "Didn't find any matches after fetcher action");
- let loadId = ++incrementingLoadId;
- fetchReloadIds.set(key, loadId);
- let loadFetcher = getLoadingFetcher(submission, actionResult.data);
- state.fetchers.set(key, loadFetcher);
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, false, future.v7_skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, [match.route.id, actionResult]);
- // Put all revalidating fetchers into the loading state, except for the
- // current fetcher which we want to keep in it's current loading state which
- // contains it's action submission info + action data
- revalidatingFetchers.filter(rf => rf.key !== key).forEach(rf => {
- let staleKey = rf.key;
- let existingFetcher = state.fetchers.get(staleKey);
- let revalidatingFetcher = getLoadingFetcher(undefined, existingFetcher ? existingFetcher.data : undefined);
- state.fetchers.set(staleKey, revalidatingFetcher);
- abortFetcher(staleKey);
- if (rf.controller) {
- fetchControllers.set(staleKey, rf.controller);
- }
- });
- updateState({
- fetchers: new Map(state.fetchers)
- });
- let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
- abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
- let {
- loaderResults,
- fetcherResults
- } = await callLoadersAndMaybeResolveData(state, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
- if (abortController.signal.aborted) {
- return;
- }
- abortController.signal.removeEventListener("abort", abortPendingFetchRevalidations);
- fetchReloadIds.delete(key);
- fetchControllers.delete(key);
- revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
- let redirect = findRedirect(loaderResults);
- if (redirect) {
- return startRedirectNavigation(revalidationRequest, redirect.result, false, {
- preventScrollReset
- });
- }
- redirect = findRedirect(fetcherResults);
- if (redirect) {
- // If this redirect came from a fetcher make sure we mark it in
- // fetchRedirectIds so it doesn't get revalidated on the next set of
- // loader executions
- fetchRedirectIds.add(redirect.key);
- return startRedirectNavigation(revalidationRequest, redirect.result, false, {
- preventScrollReset
- });
- }
- // Process and commit output from loaders
- let {
- loaderData,
- errors
- } = processLoaderData(state, matches, loaderResults, undefined, revalidatingFetchers, fetcherResults, activeDeferreds);
- // Since we let revalidations complete even if the submitting fetcher was
- // deleted, only put it back to idle if it hasn't been deleted
- if (state.fetchers.has(key)) {
- let doneFetcher = getDoneFetcher(actionResult.data);
- state.fetchers.set(key, doneFetcher);
- }
- abortStaleFetchLoads(loadId);
- // If we are currently in a navigation loading state and this fetcher is
- // more recent than the navigation, we want the newer data so abort the
- // navigation and complete it with the fetcher data
- if (state.navigation.state === "loading" && loadId > pendingNavigationLoadId) {
- invariant(pendingAction, "Expected pending action");
- pendingNavigationController && pendingNavigationController.abort();
- completeNavigation(state.navigation.location, {
- matches,
- loaderData,
- errors,
- fetchers: new Map(state.fetchers)
- });
- } else {
- // otherwise just update with the fetcher data, preserving any existing
- // loaderData for loaders that did not need to reload. We have to
- // manually merge here since we aren't going through completeNavigation
- updateState({
- errors,
- loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
- fetchers: new Map(state.fetchers)
- });
- isRevalidationRequired = false;
- }
- }
- // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
- async function handleFetcherLoader(key, routeId, path, match, matches, isFogOfWar, flushSync, preventScrollReset, submission) {
- let existingFetcher = state.fetchers.get(key);
- updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined), {
- flushSync
- });
- let abortController = new AbortController();
- let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
- if (isFogOfWar) {
- let discoverResult = await discoverRoutes(matches, new URL(fetchRequest.url).pathname, fetchRequest.signal, key);
- if (discoverResult.type === "aborted") {
- return;
- } else if (discoverResult.type === "error") {
- setFetcherError(key, routeId, discoverResult.error, {
- flushSync
- });
- return;
- } else if (!discoverResult.matches) {
- setFetcherError(key, routeId, getInternalRouterError(404, {
- pathname: path
- }), {
- flushSync
- });
- return;
- } else {
- matches = discoverResult.matches;
- match = getTargetMatch(matches, path);
- }
- }
- // Call the loader for this fetcher route match
- fetchControllers.set(key, abortController);
- let originatingLoadId = incrementingLoadId;
- let results = await callDataStrategy("loader", state, fetchRequest, [match], matches, key);
- let result = results[match.route.id];
- // Deferred isn't supported for fetcher loads, await everything and treat it
- // as a normal load. resolveDeferredData will return undefined if this
- // fetcher gets aborted, so we just leave result untouched and short circuit
- // below if that happens
- if (isDeferredResult(result)) {
- result = (await resolveDeferredData(result, fetchRequest.signal, true)) || result;
- }
- // We can delete this so long as we weren't aborted by our our own fetcher
- // re-load which would have put _new_ controller is in fetchControllers
- if (fetchControllers.get(key) === abortController) {
- fetchControllers.delete(key);
- }
- if (fetchRequest.signal.aborted) {
- return;
- }
- // We don't want errors bubbling up or redirects followed for unmounted
- // fetchers, so short circuit here if it was removed from the UI
- if (deletedFetchers.has(key)) {
- updateFetcherState(key, getDoneFetcher(undefined));
- return;
- }
- // If the loader threw a redirect Response, start a new REPLACE navigation
- if (isRedirectResult(result)) {
- if (pendingNavigationLoadId > originatingLoadId) {
- // A new navigation was kicked off after our loader started, so that
- // should take precedence over this redirect navigation
- updateFetcherState(key, getDoneFetcher(undefined));
- return;
- } else {
- fetchRedirectIds.add(key);
- await startRedirectNavigation(fetchRequest, result, false, {
- preventScrollReset
- });
- return;
- }
- }
- // Process any non-redirect errors thrown
- if (isErrorResult(result)) {
- setFetcherError(key, routeId, result.error);
- return;
- }
- invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
- // Put the fetcher back into an idle state
- updateFetcherState(key, getDoneFetcher(result.data));
- }
- /**
- * Utility function to handle redirects returned from an action or loader.
- * Normally, a redirect "replaces" the navigation that triggered it. So, for
- * example:
- *
- * - user is on /a
- * - user clicks a link to /b
- * - loader for /b redirects to /c
- *
- * In a non-JS app the browser would track the in-flight navigation to /b and
- * then replace it with /c when it encountered the redirect response. In
- * the end it would only ever update the URL bar with /c.
- *
- * In client-side routing using pushState/replaceState, we aim to emulate
- * this behavior and we also do not update history until the end of the
- * navigation (including processed redirects). This means that we never
- * actually touch history until we've processed redirects, so we just use
- * the history action from the original navigation (PUSH or REPLACE).
- */
- async function startRedirectNavigation(request, redirect, isNavigation, _temp2) {
- let {
- submission,
- fetcherSubmission,
- preventScrollReset,
- replace
- } = _temp2 === void 0 ? {} : _temp2;
- if (redirect.response.headers.has("X-Remix-Revalidate")) {
- isRevalidationRequired = true;
- }
- let location = redirect.response.headers.get("Location");
- invariant(location, "Expected a Location header on the redirect Response");
- location = normalizeRedirectLocation(location, new URL(request.url), basename);
- let redirectLocation = createLocation(state.location, location, {
- _isRedirect: true
- });
- if (isBrowser) {
- let isDocumentReload = false;
- if (redirect.response.headers.has("X-Remix-Reload-Document")) {
- // Hard reload if the response contained X-Remix-Reload-Document
- isDocumentReload = true;
- } else if (ABSOLUTE_URL_REGEX.test(location)) {
- const url = init.history.createURL(location);
- isDocumentReload =
- // Hard reload if it's an absolute URL to a new origin
- url.origin !== routerWindow.location.origin ||
- // Hard reload if it's an absolute URL that does not match our basename
- stripBasename(url.pathname, basename) == null;
- }
- if (isDocumentReload) {
- if (replace) {
- routerWindow.location.replace(location);
- } else {
- routerWindow.location.assign(location);
- }
- return;
- }
- }
- // There's no need to abort on redirects, since we don't detect the
- // redirect until the action/loaders have settled
- pendingNavigationController = null;
- let redirectHistoryAction = replace === true || redirect.response.headers.has("X-Remix-Replace") ? Action.Replace : Action.Push;
- // Use the incoming submission if provided, fallback on the active one in
- // state.navigation
- let {
- formMethod,
- formAction,
- formEncType
- } = state.navigation;
- if (!submission && !fetcherSubmission && formMethod && formAction && formEncType) {
- submission = getSubmissionFromNavigation(state.navigation);
- }
- // If this was a 307/308 submission we want to preserve the HTTP method and
- // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
- // redirected location
- let activeSubmission = submission || fetcherSubmission;
- if (redirectPreserveMethodStatusCodes.has(redirect.response.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
- await startNavigation(redirectHistoryAction, redirectLocation, {
- submission: _extends({}, activeSubmission, {
- formAction: location
- }),
- // Preserve these flags across redirects
- preventScrollReset: preventScrollReset || pendingPreventScrollReset,
- enableViewTransition: isNavigation ? pendingViewTransitionEnabled : undefined
- });
- } else {
- // If we have a navigation submission, we will preserve it through the
- // redirect navigation
- let overrideNavigation = getLoadingNavigation(redirectLocation, submission);
- await startNavigation(redirectHistoryAction, redirectLocation, {
- overrideNavigation,
- // Send fetcher submissions through for shouldRevalidate
- fetcherSubmission,
- // Preserve these flags across redirects
- preventScrollReset: preventScrollReset || pendingPreventScrollReset,
- enableViewTransition: isNavigation ? pendingViewTransitionEnabled : undefined
- });
- }
- }
- // Utility wrapper for calling dataStrategy client-side without having to
- // pass around the manifest, mapRouteProperties, etc.
- async function callDataStrategy(type, state, request, matchesToLoad, matches, fetcherKey) {
- let results;
- let dataResults = {};
- try {
- results = await callDataStrategyImpl(dataStrategyImpl, type, state, request, matchesToLoad, matches, fetcherKey, manifest, mapRouteProperties);
- } catch (e) {
- // If the outer dataStrategy method throws, just return the error for all
- // matches - and it'll naturally bubble to the root
- matchesToLoad.forEach(m => {
- dataResults[m.route.id] = {
- type: ResultType.error,
- error: e
- };
- });
- return dataResults;
- }
- for (let [routeId, result] of Object.entries(results)) {
- if (isRedirectDataStrategyResultResult(result)) {
- let response = result.result;
- dataResults[routeId] = {
- type: ResultType.redirect,
- response: normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename, future.v7_relativeSplatPath)
- };
- } else {
- dataResults[routeId] = await convertDataStrategyResultToDataResult(result);
- }
- }
- return dataResults;
- }
- async function callLoadersAndMaybeResolveData(state, matches, matchesToLoad, fetchersToLoad, request) {
- let currentMatches = state.matches;
- // Kick off loaders and fetchers in parallel
- let loaderResultsPromise = callDataStrategy("loader", state, request, matchesToLoad, matches, null);
- let fetcherResultsPromise = Promise.all(fetchersToLoad.map(async f => {
- if (f.matches && f.match && f.controller) {
- let results = await callDataStrategy("loader", state, createClientSideRequest(init.history, f.path, f.controller.signal), [f.match], f.matches, f.key);
- let result = results[f.match.route.id];
- // Fetcher results are keyed by fetcher key from here on out, not routeId
- return {
- [f.key]: result
- };
- } else {
- return Promise.resolve({
- [f.key]: {
- type: ResultType.error,
- error: getInternalRouterError(404, {
- pathname: f.path
- })
- }
- });
- }
- }));
- let loaderResults = await loaderResultsPromise;
- let fetcherResults = (await fetcherResultsPromise).reduce((acc, r) => Object.assign(acc, r), {});
- await Promise.all([resolveNavigationDeferredResults(matches, loaderResults, request.signal, currentMatches, state.loaderData), resolveFetcherDeferredResults(matches, fetcherResults, fetchersToLoad)]);
- return {
- loaderResults,
- fetcherResults
- };
- }
- function interruptActiveLoads() {
- // Every interruption triggers a revalidation
- isRevalidationRequired = true;
- // Cancel pending route-level deferreds and mark cancelled routes for
- // revalidation
- cancelledDeferredRoutes.push(...cancelActiveDeferreds());
- // Abort in-flight fetcher loads
- fetchLoadMatches.forEach((_, key) => {
- if (fetchControllers.has(key)) {
- cancelledFetcherLoads.add(key);
- }
- abortFetcher(key);
- });
- }
- function updateFetcherState(key, fetcher, opts) {
- if (opts === void 0) {
- opts = {};
- }
- state.fetchers.set(key, fetcher);
- updateState({
- fetchers: new Map(state.fetchers)
- }, {
- flushSync: (opts && opts.flushSync) === true
- });
- }
- function setFetcherError(key, routeId, error, opts) {
- if (opts === void 0) {
- opts = {};
- }
- let boundaryMatch = findNearestBoundary(state.matches, routeId);
- deleteFetcher(key);
- updateState({
- errors: {
- [boundaryMatch.route.id]: error
- },
- fetchers: new Map(state.fetchers)
- }, {
- flushSync: (opts && opts.flushSync) === true
- });
- }
- function getFetcher(key) {
- activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
- // If this fetcher was previously marked for deletion, unmark it since we
- // have a new instance
- if (deletedFetchers.has(key)) {
- deletedFetchers.delete(key);
- }
- return state.fetchers.get(key) || IDLE_FETCHER;
- }
- function deleteFetcher(key) {
- let fetcher = state.fetchers.get(key);
- // Don't abort the controller if this is a deletion of a fetcher.submit()
- // in it's loading phase since - we don't want to abort the corresponding
- // revalidation and want them to complete and land
- if (fetchControllers.has(key) && !(fetcher && fetcher.state === "loading" && fetchReloadIds.has(key))) {
- abortFetcher(key);
- }
- fetchLoadMatches.delete(key);
- fetchReloadIds.delete(key);
- fetchRedirectIds.delete(key);
- // If we opted into the flag we can clear this now since we're calling
- // deleteFetcher() at the end of updateState() and we've already handed the
- // deleted fetcher keys off to the data layer.
- // If not, we're eagerly calling deleteFetcher() and we need to keep this
- // Set populated until the next updateState call, and we'll clear
- // `deletedFetchers` then
- if (future.v7_fetcherPersist) {
- deletedFetchers.delete(key);
- }
- cancelledFetcherLoads.delete(key);
- state.fetchers.delete(key);
- }
- function deleteFetcherAndUpdateState(key) {
- let count = (activeFetchers.get(key) || 0) - 1;
- if (count <= 0) {
- activeFetchers.delete(key);
- deletedFetchers.add(key);
- if (!future.v7_fetcherPersist) {
- deleteFetcher(key);
- }
- } else {
- activeFetchers.set(key, count);
- }
- updateState({
- fetchers: new Map(state.fetchers)
- });
- }
- function abortFetcher(key) {
- let controller = fetchControllers.get(key);
- if (controller) {
- controller.abort();
- fetchControllers.delete(key);
- }
- }
- function markFetchersDone(keys) {
- for (let key of keys) {
- let fetcher = getFetcher(key);
- let doneFetcher = getDoneFetcher(fetcher.data);
- state.fetchers.set(key, doneFetcher);
- }
- }
- function markFetchRedirectsDone() {
- let doneKeys = [];
- let updatedFetchers = false;
- for (let key of fetchRedirectIds) {
- let fetcher = state.fetchers.get(key);
- invariant(fetcher, "Expected fetcher: " + key);
- if (fetcher.state === "loading") {
- fetchRedirectIds.delete(key);
- doneKeys.push(key);
- updatedFetchers = true;
- }
- }
- markFetchersDone(doneKeys);
- return updatedFetchers;
- }
- function abortStaleFetchLoads(landedId) {
- let yeetedKeys = [];
- for (let [key, id] of fetchReloadIds) {
- if (id < landedId) {
- let fetcher = state.fetchers.get(key);
- invariant(fetcher, "Expected fetcher: " + key);
- if (fetcher.state === "loading") {
- abortFetcher(key);
- fetchReloadIds.delete(key);
- yeetedKeys.push(key);
- }
- }
- }
- markFetchersDone(yeetedKeys);
- return yeetedKeys.length > 0;
- }
- function getBlocker(key, fn) {
- let blocker = state.blockers.get(key) || IDLE_BLOCKER;
- if (blockerFunctions.get(key) !== fn) {
- blockerFunctions.set(key, fn);
- }
- return blocker;
- }
- function deleteBlocker(key) {
- state.blockers.delete(key);
- blockerFunctions.delete(key);
- }
- // Utility function to update blockers, ensuring valid state transitions
- function updateBlocker(key, newBlocker) {
- let blocker = state.blockers.get(key) || IDLE_BLOCKER;
- // Poor mans state machine :)
- // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
- invariant(blocker.state === "unblocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "proceeding" || blocker.state === "blocked" && newBlocker.state === "unblocked" || blocker.state === "proceeding" && newBlocker.state === "unblocked", "Invalid blocker state transition: " + blocker.state + " -> " + newBlocker.state);
- let blockers = new Map(state.blockers);
- blockers.set(key, newBlocker);
- updateState({
- blockers
- });
- }
- function shouldBlockNavigation(_ref2) {
- let {
- currentLocation,
- nextLocation,
- historyAction
- } = _ref2;
- if (blockerFunctions.size === 0) {
- return;
- }
- // We ony support a single active blocker at the moment since we don't have
- // any compelling use cases for multi-blocker yet
- if (blockerFunctions.size > 1) {
- warning(false, "A router only supports one blocker at a time");
- }
- let entries = Array.from(blockerFunctions.entries());
- let [blockerKey, blockerFunction] = entries[entries.length - 1];
- let blocker = state.blockers.get(blockerKey);
- if (blocker && blocker.state === "proceeding") {
- // If the blocker is currently proceeding, we don't need to re-check
- // it and can let this navigation continue
- return;
- }
- // At this point, we know we're unblocked/blocked so we need to check the
- // user-provided blocker function
- if (blockerFunction({
- currentLocation,
- nextLocation,
- historyAction
- })) {
- return blockerKey;
- }
- }
- function handleNavigational404(pathname) {
- let error = getInternalRouterError(404, {
- pathname
- });
- let routesToUse = inFlightDataRoutes || dataRoutes;
- let {
- matches,
- route
- } = getShortCircuitMatches(routesToUse);
- // Cancel all pending deferred on 404s since we don't keep any routes
- cancelActiveDeferreds();
- return {
- notFoundMatches: matches,
- route,
- error
- };
- }
- function cancelActiveDeferreds(predicate) {
- let cancelledRouteIds = [];
- activeDeferreds.forEach((dfd, routeId) => {
- if (!predicate || predicate(routeId)) {
- // Cancel the deferred - but do not remove from activeDeferreds here -
- // we rely on the subscribers to do that so our tests can assert proper
- // cleanup via _internalActiveDeferreds
- dfd.cancel();
- cancelledRouteIds.push(routeId);
- activeDeferreds.delete(routeId);
- }
- });
- return cancelledRouteIds;
- }
- // Opt in to capturing and reporting scroll positions during navigations,
- // used by the <ScrollRestoration> component
- function enableScrollRestoration(positions, getPosition, getKey) {
- savedScrollPositions = positions;
- getScrollPosition = getPosition;
- getScrollRestorationKey = getKey || null;
- // Perform initial hydration scroll restoration, since we miss the boat on
- // the initial updateState() because we've not yet rendered <ScrollRestoration/>
- // and therefore have no savedScrollPositions available
- if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
- initialScrollRestored = true;
- let y = getSavedScrollPosition(state.location, state.matches);
- if (y != null) {
- updateState({
- restoreScrollPosition: y
- });
- }
- }
- return () => {
- savedScrollPositions = null;
- getScrollPosition = null;
- getScrollRestorationKey = null;
- };
- }
- function getScrollKey(location, matches) {
- if (getScrollRestorationKey) {
- let key = getScrollRestorationKey(location, matches.map(m => convertRouteMatchToUiMatch(m, state.loaderData)));
- return key || location.key;
- }
- return location.key;
- }
- function saveScrollPosition(location, matches) {
- if (savedScrollPositions && getScrollPosition) {
- let key = getScrollKey(location, matches);
- savedScrollPositions[key] = getScrollPosition();
- }
- }
- function getSavedScrollPosition(location, matches) {
- if (savedScrollPositions) {
- let key = getScrollKey(location, matches);
- let y = savedScrollPositions[key];
- if (typeof y === "number") {
- return y;
- }
- }
- return null;
- }
- function checkFogOfWar(matches, routesToUse, pathname) {
- if (patchRoutesOnNavigationImpl) {
- if (!matches) {
- let fogMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
- return {
- active: true,
- matches: fogMatches || []
- };
- } else {
- if (Object.keys(matches[0].params).length > 0) {
- // If we matched a dynamic param or a splat, it might only be because
- // we haven't yet discovered other routes that would match with a
- // higher score. Call patchRoutesOnNavigation just to be sure
- let partialMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
- return {
- active: true,
- matches: partialMatches
- };
- }
- }
- }
- return {
- active: false,
- matches: null
- };
- }
- async function discoverRoutes(matches, pathname, signal, fetcherKey) {
- if (!patchRoutesOnNavigationImpl) {
- return {
- type: "success",
- matches
- };
- }
- let partialMatches = matches;
- while (true) {
- let isNonHMR = inFlightDataRoutes == null;
- let routesToUse = inFlightDataRoutes || dataRoutes;
- let localManifest = manifest;
- try {
- await patchRoutesOnNavigationImpl({
- signal,
- path: pathname,
- matches: partialMatches,
- fetcherKey,
- patch: (routeId, children) => {
- if (signal.aborted) return;
- patchRoutesImpl(routeId, children, routesToUse, localManifest, mapRouteProperties);
- }
- });
- } catch (e) {
- return {
- type: "error",
- error: e,
- partialMatches
- };
- } finally {
- // If we are not in the middle of an HMR revalidation and we changed the
- // routes, provide a new identity so when we `updateState` at the end of
- // this navigation/fetch `router.routes` will be a new identity and
- // trigger a re-run of memoized `router.routes` dependencies.
- // HMR will already update the identity and reflow when it lands
- // `inFlightDataRoutes` in `completeNavigation`
- if (isNonHMR && !signal.aborted) {
- dataRoutes = [...dataRoutes];
- }
- }
- if (signal.aborted) {
- return {
- type: "aborted"
- };
- }
- let newMatches = matchRoutes(routesToUse, pathname, basename);
- if (newMatches) {
- return {
- type: "success",
- matches: newMatches
- };
- }
- let newPartialMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
- // Avoid loops if the second pass results in the same partial matches
- if (!newPartialMatches || partialMatches.length === newPartialMatches.length && partialMatches.every((m, i) => m.route.id === newPartialMatches[i].route.id)) {
- return {
- type: "success",
- matches: null
- };
- }
- partialMatches = newPartialMatches;
- }
- }
- function _internalSetRoutes(newRoutes) {
- manifest = {};
- inFlightDataRoutes = convertRoutesToDataRoutes(newRoutes, mapRouteProperties, undefined, manifest);
- }
- function patchRoutes(routeId, children) {
- let isNonHMR = inFlightDataRoutes == null;
- let routesToUse = inFlightDataRoutes || dataRoutes;
- patchRoutesImpl(routeId, children, routesToUse, manifest, mapRouteProperties);
- // If we are not in the middle of an HMR revalidation and we changed the
- // routes, provide a new identity and trigger a reflow via `updateState`
- // to re-run memoized `router.routes` dependencies.
- // HMR will already update the identity and reflow when it lands
- // `inFlightDataRoutes` in `completeNavigation`
- if (isNonHMR) {
- dataRoutes = [...dataRoutes];
- updateState({});
- }
- }
- router = {
- get basename() {
- return basename;
- },
- get future() {
- return future;
- },
- get state() {
- return state;
- },
- get routes() {
- return dataRoutes;
- },
- get window() {
- return routerWindow;
- },
- initialize,
- subscribe,
- enableScrollRestoration,
- navigate,
- fetch,
- revalidate,
- // Passthrough to history-aware createHref used by useHref so we get proper
- // hash-aware URLs in DOM paths
- createHref: to => init.history.createHref(to),
- encodeLocation: to => init.history.encodeLocation(to),
- getFetcher,
- deleteFetcher: deleteFetcherAndUpdateState,
- dispose,
- getBlocker,
- deleteBlocker,
- patchRoutes,
- _internalFetchControllers: fetchControllers,
- _internalActiveDeferreds: activeDeferreds,
- // TODO: Remove setRoutes, it's temporary to avoid dealing with
- // updating the tree while validating the update algorithm.
- _internalSetRoutes
- };
- return router;
- }
- //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region createStaticHandler
- ////////////////////////////////////////////////////////////////////////////////
- const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
- function createStaticHandler(routes, opts) {
- invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
- let manifest = {};
- let basename = (opts ? opts.basename : null) || "/";
- let mapRouteProperties;
- if (opts != null && opts.mapRouteProperties) {
- mapRouteProperties = opts.mapRouteProperties;
- } else if (opts != null && opts.detectErrorBoundary) {
- // If they are still using the deprecated version, wrap it with the new API
- let detectErrorBoundary = opts.detectErrorBoundary;
- mapRouteProperties = route => ({
- hasErrorBoundary: detectErrorBoundary(route)
- });
- } else {
- mapRouteProperties = defaultMapRouteProperties;
- }
- // Config driven behavior flags
- let future = _extends({
- v7_relativeSplatPath: false,
- v7_throwAbortReason: false
- }, opts ? opts.future : null);
- let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
- /**
- * The query() method is intended for document requests, in which we want to
- * call an optional action and potentially multiple loaders for all nested
- * routes. It returns a StaticHandlerContext object, which is very similar
- * to the router state (location, loaderData, actionData, errors, etc.) and
- * also adds SSR-specific information such as the statusCode and headers
- * from action/loaders Responses.
- *
- * It _should_ never throw and should report all errors through the
- * returned context.errors object, properly associating errors to their error
- * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
- * used to emulate React error boundaries during SSr by performing a second
- * pass only down to the boundaryId.
- *
- * The one exception where we do not return a StaticHandlerContext is when a
- * redirect response is returned or thrown from any action/loader. We
- * propagate that out and return the raw Response so the HTTP server can
- * return it directly.
- *
- * - `opts.requestContext` is an optional server context that will be passed
- * to actions/loaders in the `context` parameter
- * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
- * the bubbling of errors which allows single-fetch-type implementations
- * where the client will handle the bubbling and we may need to return data
- * for the handling route
- */
- async function query(request, _temp3) {
- let {
- requestContext,
- skipLoaderErrorBubbling,
- dataStrategy
- } = _temp3 === void 0 ? {} : _temp3;
- let url = new URL(request.url);
- let method = request.method;
- let location = createLocation("", createPath(url), null, "default");
- let matches = matchRoutes(dataRoutes, location, basename);
- // SSR supports HEAD requests while SPA doesn't
- if (!isValidMethod(method) && method !== "HEAD") {
- let error = getInternalRouterError(405, {
- method
- });
- let {
- matches: methodNotAllowedMatches,
- route
- } = getShortCircuitMatches(dataRoutes);
- return {
- basename,
- location,
- matches: methodNotAllowedMatches,
- loaderData: {},
- actionData: null,
- errors: {
- [route.id]: error
- },
- statusCode: error.status,
- loaderHeaders: {},
- actionHeaders: {},
- activeDeferreds: null
- };
- } else if (!matches) {
- let error = getInternalRouterError(404, {
- pathname: location.pathname
- });
- let {
- matches: notFoundMatches,
- route
- } = getShortCircuitMatches(dataRoutes);
- return {
- basename,
- location,
- matches: notFoundMatches,
- loaderData: {},
- actionData: null,
- errors: {
- [route.id]: error
- },
- statusCode: error.status,
- loaderHeaders: {},
- actionHeaders: {},
- activeDeferreds: null
- };
- }
- let result = await queryImpl(request, location, matches, requestContext, dataStrategy || null, skipLoaderErrorBubbling === true, null);
- if (isResponse(result)) {
- return result;
- }
- // When returning StaticHandlerContext, we patch back in the location here
- // since we need it for React Context. But this helps keep our submit and
- // loadRouteData operating on a Request instead of a Location
- return _extends({
- location,
- basename
- }, result);
- }
- /**
- * The queryRoute() method is intended for targeted route requests, either
- * for fetch ?_data requests or resource route requests. In this case, we
- * are only ever calling a single action or loader, and we are returning the
- * returned value directly. In most cases, this will be a Response returned
- * from the action/loader, but it may be a primitive or other value as well -
- * and in such cases the calling context should handle that accordingly.
- *
- * We do respect the throw/return differentiation, so if an action/loader
- * throws, then this method will throw the value. This is important so we
- * can do proper boundary identification in Remix where a thrown Response
- * must go to the Catch Boundary but a returned Response is happy-path.
- *
- * One thing to note is that any Router-initiated Errors that make sense
- * to associate with a status code will be thrown as an ErrorResponse
- * instance which include the raw Error, such that the calling context can
- * serialize the error as they see fit while including the proper response
- * code. Examples here are 404 and 405 errors that occur prior to reaching
- * any user-defined loaders.
- *
- * - `opts.routeId` allows you to specify the specific route handler to call.
- * If not provided the handler will determine the proper route by matching
- * against `request.url`
- * - `opts.requestContext` is an optional server context that will be passed
- * to actions/loaders in the `context` parameter
- */
- async function queryRoute(request, _temp4) {
- let {
- routeId,
- requestContext,
- dataStrategy
- } = _temp4 === void 0 ? {} : _temp4;
- let url = new URL(request.url);
- let method = request.method;
- let location = createLocation("", createPath(url), null, "default");
- let matches = matchRoutes(dataRoutes, location, basename);
- // SSR supports HEAD requests while SPA doesn't
- if (!isValidMethod(method) && method !== "HEAD" && method !== "OPTIONS") {
- throw getInternalRouterError(405, {
- method
- });
- } else if (!matches) {
- throw getInternalRouterError(404, {
- pathname: location.pathname
- });
- }
- let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);
- if (routeId && !match) {
- throw getInternalRouterError(403, {
- pathname: location.pathname,
- routeId
- });
- } else if (!match) {
- // This should never hit I don't think?
- throw getInternalRouterError(404, {
- pathname: location.pathname
- });
- }
- let result = await queryImpl(request, location, matches, requestContext, dataStrategy || null, false, match);
- if (isResponse(result)) {
- return result;
- }
- let error = result.errors ? Object.values(result.errors)[0] : undefined;
- if (error !== undefined) {
- // If we got back result.errors, that means the loader/action threw
- // _something_ that wasn't a Response, but it's not guaranteed/required
- // to be an `instanceof Error` either, so we have to use throw here to
- // preserve the "error" state outside of queryImpl.
- throw error;
- }
- // Pick off the right state value to return
- if (result.actionData) {
- return Object.values(result.actionData)[0];
- }
- if (result.loaderData) {
- var _result$activeDeferre;
- let data = Object.values(result.loaderData)[0];
- if ((_result$activeDeferre = result.activeDeferreds) != null && _result$activeDeferre[match.route.id]) {
- data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
- }
- return data;
- }
- return undefined;
- }
- async function queryImpl(request, location, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch) {
- invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
- try {
- if (isMutationMethod(request.method.toLowerCase())) {
- let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch != null);
- return result;
- }
- let result = await loadRouteData(request, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch);
- return isResponse(result) ? result : _extends({}, result, {
- actionData: null,
- actionHeaders: {}
- });
- } catch (e) {
- // If the user threw/returned a Response in callLoaderOrAction for a
- // `queryRoute` call, we throw the `DataStrategyResult` to bail out early
- // and then return or throw the raw Response here accordingly
- if (isDataStrategyResult(e) && isResponse(e.result)) {
- if (e.type === ResultType.error) {
- throw e.result;
- }
- return e.result;
- }
- // Redirects are always returned since they don't propagate to catch
- // boundaries
- if (isRedirectResponse(e)) {
- return e;
- }
- throw e;
- }
- }
- async function submit(request, matches, actionMatch, requestContext, dataStrategy, skipLoaderErrorBubbling, isRouteRequest) {
- let result;
- if (!actionMatch.route.action && !actionMatch.route.lazy) {
- let error = getInternalRouterError(405, {
- method: request.method,
- pathname: new URL(request.url).pathname,
- routeId: actionMatch.route.id
- });
- if (isRouteRequest) {
- throw error;
- }
- result = {
- type: ResultType.error,
- error
- };
- } else {
- let results = await callDataStrategy("action", request, [actionMatch], matches, isRouteRequest, requestContext, dataStrategy);
- result = results[actionMatch.route.id];
- if (request.signal.aborted) {
- throwStaticHandlerAbortedError(request, isRouteRequest, future);
- }
- }
- if (isRedirectResult(result)) {
- // Uhhhh - this should never happen, we should always throw these from
- // callLoaderOrAction, but the type narrowing here keeps TS happy and we
- // can get back on the "throw all redirect responses" train here should
- // this ever happen :/
- throw new Response(null, {
- status: result.response.status,
- headers: {
- Location: result.response.headers.get("Location")
- }
- });
- }
- if (isDeferredResult(result)) {
- let error = getInternalRouterError(400, {
- type: "defer-action"
- });
- if (isRouteRequest) {
- throw error;
- }
- result = {
- type: ResultType.error,
- error
- };
- }
- if (isRouteRequest) {
- // Note: This should only be non-Response values if we get here, since
- // isRouteRequest should throw any Response received in callLoaderOrAction
- if (isErrorResult(result)) {
- throw result.error;
- }
- return {
- matches: [actionMatch],
- loaderData: {},
- actionData: {
- [actionMatch.route.id]: result.data
- },
- errors: null,
- // Note: statusCode + headers are unused here since queryRoute will
- // return the raw Response or value
- statusCode: 200,
- loaderHeaders: {},
- actionHeaders: {},
- activeDeferreds: null
- };
- }
- // Create a GET request for the loaders
- let loaderRequest = new Request(request.url, {
- headers: request.headers,
- redirect: request.redirect,
- signal: request.signal
- });
- if (isErrorResult(result)) {
- // Store off the pending error - we use it to determine which loaders
- // to call and will commit it when we complete the navigation
- let boundaryMatch = skipLoaderErrorBubbling ? actionMatch : findNearestBoundary(matches, actionMatch.route.id);
- let context = await loadRouteData(loaderRequest, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, null, [boundaryMatch.route.id, result]);
- // action status codes take precedence over loader status codes
- return _extends({}, context, {
- statusCode: isRouteErrorResponse(result.error) ? result.error.status : result.statusCode != null ? result.statusCode : 500,
- actionData: null,
- actionHeaders: _extends({}, result.headers ? {
- [actionMatch.route.id]: result.headers
- } : {})
- });
- }
- let context = await loadRouteData(loaderRequest, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, null);
- return _extends({}, context, {
- actionData: {
- [actionMatch.route.id]: result.data
- }
- }, result.statusCode ? {
- statusCode: result.statusCode
- } : {}, {
- actionHeaders: result.headers ? {
- [actionMatch.route.id]: result.headers
- } : {}
- });
- }
- async function loadRouteData(request, matches, requestContext, dataStrategy, skipLoaderErrorBubbling, routeMatch, pendingActionResult) {
- let isRouteRequest = routeMatch != null;
- // Short circuit if we have no loaders to run (queryRoute())
- if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader) && !(routeMatch != null && routeMatch.route.lazy)) {
- throw getInternalRouterError(400, {
- method: request.method,
- pathname: new URL(request.url).pathname,
- routeId: routeMatch == null ? void 0 : routeMatch.route.id
- });
- }
- let requestMatches = routeMatch ? [routeMatch] : pendingActionResult && isErrorResult(pendingActionResult[1]) ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0]) : matches;
- let matchesToLoad = requestMatches.filter(m => m.route.loader || m.route.lazy);
- // Short circuit if we have no loaders to run (query())
- if (matchesToLoad.length === 0) {
- return {
- matches,
- // Add a null for all matched routes for proper revalidation on the client
- loaderData: matches.reduce((acc, m) => Object.assign(acc, {
- [m.route.id]: null
- }), {}),
- errors: pendingActionResult && isErrorResult(pendingActionResult[1]) ? {
- [pendingActionResult[0]]: pendingActionResult[1].error
- } : null,
- statusCode: 200,
- loaderHeaders: {},
- activeDeferreds: null
- };
- }
- let results = await callDataStrategy("loader", request, matchesToLoad, matches, isRouteRequest, requestContext, dataStrategy);
- if (request.signal.aborted) {
- throwStaticHandlerAbortedError(request, isRouteRequest, future);
- }
- // Process and commit output from loaders
- let activeDeferreds = new Map();
- let context = processRouteLoaderData(matches, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling);
- // Add a null for any non-loader matches for proper revalidation on the client
- let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));
- matches.forEach(match => {
- if (!executedLoaders.has(match.route.id)) {
- context.loaderData[match.route.id] = null;
- }
- });
- return _extends({}, context, {
- matches,
- activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
- });
- }
- // Utility wrapper for calling dataStrategy server-side without having to
- // pass around the manifest, mapRouteProperties, etc.
- async function callDataStrategy(type, request, matchesToLoad, matches, isRouteRequest, requestContext, dataStrategy) {
- let results = await callDataStrategyImpl(dataStrategy || defaultDataStrategy, type, null, request, matchesToLoad, matches, null, manifest, mapRouteProperties, requestContext);
- let dataResults = {};
- await Promise.all(matches.map(async match => {
- if (!(match.route.id in results)) {
- return;
- }
- let result = results[match.route.id];
- if (isRedirectDataStrategyResultResult(result)) {
- let response = result.result;
- // Throw redirects and let the server handle them with an HTTP redirect
- throw normalizeRelativeRoutingRedirectResponse(response, request, match.route.id, matches, basename, future.v7_relativeSplatPath);
- }
- if (isResponse(result.result) && isRouteRequest) {
- // For SSR single-route requests, we want to hand Responses back
- // directly without unwrapping
- throw result;
- }
- dataResults[match.route.id] = await convertDataStrategyResultToDataResult(result);
- }));
- return dataResults;
- }
- return {
- dataRoutes,
- query,
- queryRoute
- };
- }
- //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region Helpers
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * Given an existing StaticHandlerContext and an error thrown at render time,
- * provide an updated StaticHandlerContext suitable for a second SSR render
- */
- function getStaticContextFromError(routes, context, error) {
- let newContext = _extends({}, context, {
- statusCode: isRouteErrorResponse(error) ? error.status : 500,
- errors: {
- [context._deepestRenderedBoundaryId || routes[0].id]: error
- }
- });
- return newContext;
- }
- function throwStaticHandlerAbortedError(request, isRouteRequest, future) {
- if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
- throw request.signal.reason;
- }
- let method = isRouteRequest ? "queryRoute" : "query";
- throw new Error(method + "() call aborted: " + request.method + " " + request.url);
- }
- function isSubmissionNavigation(opts) {
- return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
- }
- function normalizeTo(location, matches, basename, prependBasename, to, v7_relativeSplatPath, fromRouteId, relative) {
- let contextualMatches;
- let activeRouteMatch;
- if (fromRouteId) {
- // Grab matches up to the calling route so our route-relative logic is
- // relative to the correct source route
- contextualMatches = [];
- for (let match of matches) {
- contextualMatches.push(match);
- if (match.route.id === fromRouteId) {
- activeRouteMatch = match;
- break;
- }
- }
- } else {
- contextualMatches = matches;
- activeRouteMatch = matches[matches.length - 1];
- }
- // Resolve the relative path
- let path = resolveTo(to ? to : ".", getResolveToMatches(contextualMatches, v7_relativeSplatPath), stripBasename(location.pathname, basename) || location.pathname, relative === "path");
- // When `to` is not specified we inherit search/hash from the current
- // location, unlike when to="." and we just inherit the path.
- // See https://github.com/remix-run/remix/issues/927
- if (to == null) {
- path.search = location.search;
- path.hash = location.hash;
- }
- // Account for `?index` params when routing to the current location
- if ((to == null || to === "" || to === ".") && activeRouteMatch) {
- let nakedIndex = hasNakedIndexQuery(path.search);
- if (activeRouteMatch.route.index && !nakedIndex) {
- // Add one when we're targeting an index route
- path.search = path.search ? path.search.replace(/^\?/, "?index&") : "?index";
- } else if (!activeRouteMatch.route.index && nakedIndex) {
- // Remove existing ones when we're not
- let params = new URLSearchParams(path.search);
- let indexValues = params.getAll("index");
- params.delete("index");
- indexValues.filter(v => v).forEach(v => params.append("index", v));
- let qs = params.toString();
- path.search = qs ? "?" + qs : "";
- }
- }
- // If we're operating within a basename, prepend it to the pathname. If
- // this is a root navigation, then just use the raw basename which allows
- // the basename to have full control over the presence of a trailing slash
- // on root actions
- if (prependBasename && basename !== "/") {
- path.pathname = path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
- }
- return createPath(path);
- }
- // Normalize navigation options by converting formMethod=GET formData objects to
- // URLSearchParams so they behave identically to links with query params
- function normalizeNavigateOptions(normalizeFormMethod, isFetcher, path, opts) {
- // Return location verbatim on non-submission navigations
- if (!opts || !isSubmissionNavigation(opts)) {
- return {
- path
- };
- }
- if (opts.formMethod && !isValidMethod(opts.formMethod)) {
- return {
- path,
- error: getInternalRouterError(405, {
- method: opts.formMethod
- })
- };
- }
- let getInvalidBodyError = () => ({
- path,
- error: getInternalRouterError(400, {
- type: "invalid-body"
- })
- });
- // Create a Submission on non-GET navigations
- let rawFormMethod = opts.formMethod || "get";
- let formMethod = normalizeFormMethod ? rawFormMethod.toUpperCase() : rawFormMethod.toLowerCase();
- let formAction = stripHashFromPath(path);
- if (opts.body !== undefined) {
- if (opts.formEncType === "text/plain") {
- // text only support POST/PUT/PATCH/DELETE submissions
- if (!isMutationMethod(formMethod)) {
- return getInvalidBodyError();
- }
- let text = typeof opts.body === "string" ? opts.body : opts.body instanceof FormData || opts.body instanceof URLSearchParams ?
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
- Array.from(opts.body.entries()).reduce((acc, _ref3) => {
- let [name, value] = _ref3;
- return "" + acc + name + "=" + value + "\n";
- }, "") : String(opts.body);
- return {
- path,
- submission: {
- formMethod,
- formAction,
- formEncType: opts.formEncType,
- formData: undefined,
- json: undefined,
- text
- }
- };
- } else if (opts.formEncType === "application/json") {
- // json only supports POST/PUT/PATCH/DELETE submissions
- if (!isMutationMethod(formMethod)) {
- return getInvalidBodyError();
- }
- try {
- let json = typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
- return {
- path,
- submission: {
- formMethod,
- formAction,
- formEncType: opts.formEncType,
- formData: undefined,
- json,
- text: undefined
- }
- };
- } catch (e) {
- return getInvalidBodyError();
- }
- }
- }
- invariant(typeof FormData === "function", "FormData is not available in this environment");
- let searchParams;
- let formData;
- if (opts.formData) {
- searchParams = convertFormDataToSearchParams(opts.formData);
- formData = opts.formData;
- } else if (opts.body instanceof FormData) {
- searchParams = convertFormDataToSearchParams(opts.body);
- formData = opts.body;
- } else if (opts.body instanceof URLSearchParams) {
- searchParams = opts.body;
- formData = convertSearchParamsToFormData(searchParams);
- } else if (opts.body == null) {
- searchParams = new URLSearchParams();
- formData = new FormData();
- } else {
- try {
- searchParams = new URLSearchParams(opts.body);
- formData = convertSearchParamsToFormData(searchParams);
- } catch (e) {
- return getInvalidBodyError();
- }
- }
- let submission = {
- formMethod,
- formAction,
- formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
- formData,
- json: undefined,
- text: undefined
- };
- if (isMutationMethod(submission.formMethod)) {
- return {
- path,
- submission
- };
- }
- // Flatten submission onto URLSearchParams for GET submissions
- let parsedPath = parsePath(path);
- // On GET navigation submissions we can drop the ?index param from the
- // resulting location since all loaders will run. But fetcher GET submissions
- // only run a single loader so we need to preserve any incoming ?index params
- if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
- searchParams.append("index", "");
- }
- parsedPath.search = "?" + searchParams;
- return {
- path: createPath(parsedPath),
- submission
- };
- }
- // Filter out all routes at/below any caught error as they aren't going to
- // render so we don't need to load them
- function getLoaderMatchesUntilBoundary(matches, boundaryId, includeBoundary) {
- if (includeBoundary === void 0) {
- includeBoundary = false;
- }
- let index = matches.findIndex(m => m.route.id === boundaryId);
- if (index >= 0) {
- return matches.slice(0, includeBoundary ? index + 1 : index);
- }
- return matches;
- }
- function getMatchesToLoad(history, state, matches, submission, location, initialHydration, skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult) {
- let actionResult = pendingActionResult ? isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : pendingActionResult[1].data : undefined;
- let currentUrl = history.createURL(state.location);
- let nextUrl = history.createURL(location);
- // Pick navigation matches that are net-new or qualify for revalidation
- let boundaryMatches = matches;
- if (initialHydration && state.errors) {
- // On initial hydration, only consider matches up to _and including_ the boundary.
- // This is inclusive to handle cases where a server loader ran successfully,
- // a child server loader bubbled up to this route, but this route has
- // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
- // we have a complete version of `loaderData`
- boundaryMatches = getLoaderMatchesUntilBoundary(matches, Object.keys(state.errors)[0], true);
- } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {
- // If an action threw an error, we call loaders up to, but not including the
- // boundary
- boundaryMatches = getLoaderMatchesUntilBoundary(matches, pendingActionResult[0]);
- }
- // Don't revalidate loaders by default after action 4xx/5xx responses
- // when the flag is enabled. They can still opt-into revalidation via
- // `shouldRevalidate` via `actionResult`
- let actionStatus = pendingActionResult ? pendingActionResult[1].statusCode : undefined;
- let shouldSkipRevalidation = skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
- let navigationMatches = boundaryMatches.filter((match, index) => {
- let {
- route
- } = match;
- if (route.lazy) {
- // We haven't loaded this route yet so we don't know if it's got a loader!
- return true;
- }
- if (route.loader == null) {
- return false;
- }
- if (initialHydration) {
- return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
- }
- // Always call the loader on new route instances and pending defer cancellations
- if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
- return true;
- }
- // This is the default implementation for when we revalidate. If the route
- // provides it's own implementation, then we give them full control but
- // provide this value so they can leverage it if needed after they check
- // their own specific use cases
- let currentRouteMatch = state.matches[index];
- let nextRouteMatch = match;
- return shouldRevalidateLoader(match, _extends({
- currentUrl,
- currentParams: currentRouteMatch.params,
- nextUrl,
- nextParams: nextRouteMatch.params
- }, submission, {
- actionResult,
- actionStatus,
- defaultShouldRevalidate: shouldSkipRevalidation ? false :
- // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
- isRevalidationRequired || currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search ||
- // Search params affect all loaders
- currentUrl.search !== nextUrl.search || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
- }));
- });
- // Pick fetcher.loads that need to be revalidated
- let revalidatingFetchers = [];
- fetchLoadMatches.forEach((f, key) => {
- // Don't revalidate:
- // - on initial hydration (shouldn't be any fetchers then anyway)
- // - if fetcher won't be present in the subsequent render
- // - no longer matches the URL (v7_fetcherPersist=false)
- // - was unmounted but persisted due to v7_fetcherPersist=true
- if (initialHydration || !matches.some(m => m.route.id === f.routeId) || deletedFetchers.has(key)) {
- return;
- }
- let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
- // If the fetcher path no longer matches, push it in with null matches so
- // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is
- // currently only a use-case for Remix HMR where the route tree can change
- // at runtime and remove a route previously loaded via a fetcher
- if (!fetcherMatches) {
- revalidatingFetchers.push({
- key,
- routeId: f.routeId,
- path: f.path,
- matches: null,
- match: null,
- controller: null
- });
- return;
- }
- // Revalidating fetchers are decoupled from the route matches since they
- // load from a static href. They revalidate based on explicit revalidation
- // (submission, useRevalidator, or X-Remix-Revalidate)
- let fetcher = state.fetchers.get(key);
- let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
- let shouldRevalidate = false;
- if (fetchRedirectIds.has(key)) {
- // Never trigger a revalidation of an actively redirecting fetcher
- shouldRevalidate = false;
- } else if (cancelledFetcherLoads.has(key)) {
- // Always mark for revalidation if the fetcher was cancelled
- cancelledFetcherLoads.delete(key);
- shouldRevalidate = true;
- } else if (fetcher && fetcher.state !== "idle" && fetcher.data === undefined) {
- // If the fetcher hasn't ever completed loading yet, then this isn't a
- // revalidation, it would just be a brand new load if an explicit
- // revalidation is required
- shouldRevalidate = isRevalidationRequired;
- } else {
- // Otherwise fall back on any user-defined shouldRevalidate, defaulting
- // to explicit revalidations only
- shouldRevalidate = shouldRevalidateLoader(fetcherMatch, _extends({
- currentUrl,
- currentParams: state.matches[state.matches.length - 1].params,
- nextUrl,
- nextParams: matches[matches.length - 1].params
- }, submission, {
- actionResult,
- actionStatus,
- defaultShouldRevalidate: shouldSkipRevalidation ? false : isRevalidationRequired
- }));
- }
- if (shouldRevalidate) {
- revalidatingFetchers.push({
- key,
- routeId: f.routeId,
- path: f.path,
- matches: fetcherMatches,
- match: fetcherMatch,
- controller: new AbortController()
- });
- }
- });
- return [navigationMatches, revalidatingFetchers];
- }
- function shouldLoadRouteOnHydration(route, loaderData, errors) {
- // We dunno if we have a loader - gotta find out!
- if (route.lazy) {
- return true;
- }
- // No loader, nothing to initialize
- if (!route.loader) {
- return false;
- }
- let hasData = loaderData != null && loaderData[route.id] !== undefined;
- let hasError = errors != null && errors[route.id] !== undefined;
- // Don't run if we error'd during SSR
- if (!hasData && hasError) {
- return false;
- }
- // Explicitly opting-in to running on hydration
- if (typeof route.loader === "function" && route.loader.hydrate === true) {
- return true;
- }
- // Otherwise, run if we're not yet initialized with anything
- return !hasData && !hasError;
- }
- function isNewLoader(currentLoaderData, currentMatch, match) {
- let isNew =
- // [a] -> [a, b]
- !currentMatch ||
- // [a, b] -> [a, c]
- match.route.id !== currentMatch.route.id;
- // Handle the case that we don't have data for a re-used route, potentially
- // from a prior error or from a cancelled pending deferred
- let isMissingData = currentLoaderData[match.route.id] === undefined;
- // Always load if this is a net-new route or we don't yet have data
- return isNew || isMissingData;
- }
- function isNewRouteInstance(currentMatch, match) {
- let currentPath = currentMatch.route.path;
- return (
- // param change for this match, /users/123 -> /users/456
- currentMatch.pathname !== match.pathname ||
- // splat param changed, which is not present in match.path
- // e.g. /files/images/avatar.jpg -> files/finances.xls
- currentPath != null && currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"]
- );
- }
- function shouldRevalidateLoader(loaderMatch, arg) {
- if (loaderMatch.route.shouldRevalidate) {
- let routeChoice = loaderMatch.route.shouldRevalidate(arg);
- if (typeof routeChoice === "boolean") {
- return routeChoice;
- }
- }
- return arg.defaultShouldRevalidate;
- }
- function patchRoutesImpl(routeId, children, routesToUse, manifest, mapRouteProperties) {
- var _childrenToPatch;
- let childrenToPatch;
- if (routeId) {
- let route = manifest[routeId];
- invariant(route, "No route found to patch children into: routeId = " + routeId);
- if (!route.children) {
- route.children = [];
- }
- childrenToPatch = route.children;
- } else {
- childrenToPatch = routesToUse;
- }
- // Don't patch in routes we already know about so that `patch` is idempotent
- // to simplify user-land code. This is useful because we re-call the
- // `patchRoutesOnNavigation` function for matched routes with params.
- let uniqueChildren = children.filter(newRoute => !childrenToPatch.some(existingRoute => isSameRoute(newRoute, existingRoute)));
- let newRoutes = convertRoutesToDataRoutes(uniqueChildren, mapRouteProperties, [routeId || "_", "patch", String(((_childrenToPatch = childrenToPatch) == null ? void 0 : _childrenToPatch.length) || "0")], manifest);
- childrenToPatch.push(...newRoutes);
- }
- function isSameRoute(newRoute, existingRoute) {
- // Most optimal check is by id
- if ("id" in newRoute && "id" in existingRoute && newRoute.id === existingRoute.id) {
- return true;
- }
- // Second is by pathing differences
- if (!(newRoute.index === existingRoute.index && newRoute.path === existingRoute.path && newRoute.caseSensitive === existingRoute.caseSensitive)) {
- return false;
- }
- // Pathless layout routes are trickier since we need to check children.
- // If they have no children then they're the same as far as we can tell
- if ((!newRoute.children || newRoute.children.length === 0) && (!existingRoute.children || existingRoute.children.length === 0)) {
- return true;
- }
- // Otherwise, we look to see if every child in the new route is already
- // represented in the existing route's children
- return newRoute.children.every((aChild, i) => {
- var _existingRoute$childr;
- return (_existingRoute$childr = existingRoute.children) == null ? void 0 : _existingRoute$childr.some(bChild => isSameRoute(aChild, bChild));
- });
- }
- /**
- * Execute route.lazy() methods to lazily load route modules (loader, action,
- * shouldRevalidate) and update the routeManifest in place which shares objects
- * with dataRoutes so those get updated as well.
- */
- async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
- if (!route.lazy) {
- return;
- }
- let lazyRoute = await route.lazy();
- // If the lazy route function was executed and removed by another parallel
- // call then we can return - first lazy() to finish wins because the return
- // value of lazy is expected to be static
- if (!route.lazy) {
- return;
- }
- let routeToUpdate = manifest[route.id];
- invariant(routeToUpdate, "No route found in manifest");
- // Update the route in place. This should be safe because there's no way
- // we could yet be sitting on this route as we can't get there without
- // resolving lazy() first.
- //
- // This is different than the HMR "update" use-case where we may actively be
- // on the route being updated. The main concern boils down to "does this
- // mutation affect any ongoing navigations or any current state.matches
- // values?". If not, it should be safe to update in place.
- let routeUpdates = {};
- for (let lazyRouteProperty in lazyRoute) {
- let staticRouteValue = routeToUpdate[lazyRouteProperty];
- let isPropertyStaticallyDefined = staticRouteValue !== undefined &&
- // This property isn't static since it should always be updated based
- // on the route updates
- lazyRouteProperty !== "hasErrorBoundary";
- warning(!isPropertyStaticallyDefined, "Route \"" + routeToUpdate.id + "\" has a static property \"" + lazyRouteProperty + "\" " + "defined but its lazy function is also returning a value for this property. " + ("The lazy route property \"" + lazyRouteProperty + "\" will be ignored."));
- if (!isPropertyStaticallyDefined && !immutableRouteKeys.has(lazyRouteProperty)) {
- routeUpdates[lazyRouteProperty] = lazyRoute[lazyRouteProperty];
- }
- }
- // Mutate the route with the provided updates. Do this first so we pass
- // the updated version to mapRouteProperties
- Object.assign(routeToUpdate, routeUpdates);
- // Mutate the `hasErrorBoundary` property on the route based on the route
- // updates and remove the `lazy` function so we don't resolve the lazy
- // route again.
- Object.assign(routeToUpdate, _extends({}, mapRouteProperties(routeToUpdate), {
- lazy: undefined
- }));
- }
- // Default implementation of `dataStrategy` which fetches all loaders in parallel
- async function defaultDataStrategy(_ref4) {
- let {
- matches
- } = _ref4;
- let matchesToLoad = matches.filter(m => m.shouldLoad);
- let results = await Promise.all(matchesToLoad.map(m => m.resolve()));
- return results.reduce((acc, result, i) => Object.assign(acc, {
- [matchesToLoad[i].route.id]: result
- }), {});
- }
- async function callDataStrategyImpl(dataStrategyImpl, type, state, request, matchesToLoad, matches, fetcherKey, manifest, mapRouteProperties, requestContext) {
- let loadRouteDefinitionsPromises = matches.map(m => m.route.lazy ? loadLazyRouteModule(m.route, mapRouteProperties, manifest) : undefined);
- let dsMatches = matches.map((match, i) => {
- let loadRoutePromise = loadRouteDefinitionsPromises[i];
- let shouldLoad = matchesToLoad.some(m => m.route.id === match.route.id);
- // `resolve` encapsulates route.lazy(), executing the loader/action,
- // and mapping return values/thrown errors to a `DataStrategyResult`. Users
- // can pass a callback to take fine-grained control over the execution
- // of the loader/action
- let resolve = async handlerOverride => {
- if (handlerOverride && request.method === "GET" && (match.route.lazy || match.route.loader)) {
- shouldLoad = true;
- }
- return shouldLoad ? callLoaderOrAction(type, request, match, loadRoutePromise, handlerOverride, requestContext) : Promise.resolve({
- type: ResultType.data,
- result: undefined
- });
- };
- return _extends({}, match, {
- shouldLoad,
- resolve
- });
- });
- // Send all matches here to allow for a middleware-type implementation.
- // handler will be a no-op for unneeded routes and we filter those results
- // back out below.
- let results = await dataStrategyImpl({
- matches: dsMatches,
- request,
- params: matches[0].params,
- fetcherKey,
- context: requestContext
- });
- // Wait for all routes to load here but 'swallow the error since we want
- // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -
- // called from `match.resolve()`
- try {
- await Promise.all(loadRouteDefinitionsPromises);
- } catch (e) {
- // No-op
- }
- return results;
- }
- // Default logic for calling a loader/action is the user has no specified a dataStrategy
- async function callLoaderOrAction(type, request, match, loadRoutePromise, handlerOverride, staticContext) {
- let result;
- let onReject;
- let runHandler = handler => {
- // Setup a promise we can race against so that abort signals short circuit
- let reject;
- // This will never resolve so safe to type it as Promise<DataStrategyResult> to
- // satisfy the function return value
- let abortPromise = new Promise((_, r) => reject = r);
- onReject = () => reject();
- request.signal.addEventListener("abort", onReject);
- let actualHandler = ctx => {
- if (typeof handler !== "function") {
- return Promise.reject(new Error("You cannot call the handler for a route which defines a boolean " + ("\"" + type + "\" [routeId: " + match.route.id + "]")));
- }
- return handler({
- request,
- params: match.params,
- context: staticContext
- }, ...(ctx !== undefined ? [ctx] : []));
- };
- let handlerPromise = (async () => {
- try {
- let val = await (handlerOverride ? handlerOverride(ctx => actualHandler(ctx)) : actualHandler());
- return {
- type: "data",
- result: val
- };
- } catch (e) {
- return {
- type: "error",
- result: e
- };
- }
- })();
- return Promise.race([handlerPromise, abortPromise]);
- };
- try {
- let handler = match.route[type];
- // If we have a route.lazy promise, await that first
- if (loadRoutePromise) {
- if (handler) {
- // Run statically defined handler in parallel with lazy()
- let handlerError;
- let [value] = await Promise.all([
- // If the handler throws, don't let it immediately bubble out,
- // since we need to let the lazy() execution finish so we know if this
- // route has a boundary that can handle the error
- runHandler(handler).catch(e => {
- handlerError = e;
- }), loadRoutePromise]);
- if (handlerError !== undefined) {
- throw handlerError;
- }
- result = value;
- } else {
- // Load lazy route module, then run any returned handler
- await loadRoutePromise;
- handler = match.route[type];
- if (handler) {
- // Handler still runs even if we got interrupted to maintain consistency
- // with un-abortable behavior of handler execution on non-lazy or
- // previously-lazy-loaded routes
- result = await runHandler(handler);
- } else if (type === "action") {
- let url = new URL(request.url);
- let pathname = url.pathname + url.search;
- throw getInternalRouterError(405, {
- method: request.method,
- pathname,
- routeId: match.route.id
- });
- } else {
- // lazy() route has no loader to run. Short circuit here so we don't
- // hit the invariant below that errors on returning undefined.
- return {
- type: ResultType.data,
- result: undefined
- };
- }
- }
- } else if (!handler) {
- let url = new URL(request.url);
- let pathname = url.pathname + url.search;
- throw getInternalRouterError(404, {
- pathname
- });
- } else {
- result = await runHandler(handler);
- }
- invariant(result.result !== undefined, "You defined " + (type === "action" ? "an action" : "a loader") + " for route " + ("\"" + match.route.id + "\" but didn't return anything from your `" + type + "` ") + "function. Please return a value or `null`.");
- } catch (e) {
- // We should already be catching and converting normal handler executions to
- // DataStrategyResults and returning them, so anything that throws here is an
- // unexpected error we still need to wrap
- return {
- type: ResultType.error,
- result: e
- };
- } finally {
- if (onReject) {
- request.signal.removeEventListener("abort", onReject);
- }
- }
- return result;
- }
- async function convertDataStrategyResultToDataResult(dataStrategyResult) {
- let {
- result,
- type
- } = dataStrategyResult;
- if (isResponse(result)) {
- let data;
- try {
- let contentType = result.headers.get("Content-Type");
- // Check between word boundaries instead of startsWith() due to the last
- // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
- if (contentType && /\bapplication\/json\b/.test(contentType)) {
- if (result.body == null) {
- data = null;
- } else {
- data = await result.json();
- }
- } else {
- data = await result.text();
- }
- } catch (e) {
- return {
- type: ResultType.error,
- error: e
- };
- }
- if (type === ResultType.error) {
- return {
- type: ResultType.error,
- error: new ErrorResponseImpl(result.status, result.statusText, data),
- statusCode: result.status,
- headers: result.headers
- };
- }
- return {
- type: ResultType.data,
- data,
- statusCode: result.status,
- headers: result.headers
- };
- }
- if (type === ResultType.error) {
- if (isDataWithResponseInit(result)) {
- var _result$init3, _result$init4;
- if (result.data instanceof Error) {
- var _result$init, _result$init2;
- return {
- type: ResultType.error,
- error: result.data,
- statusCode: (_result$init = result.init) == null ? void 0 : _result$init.status,
- headers: (_result$init2 = result.init) != null && _result$init2.headers ? new Headers(result.init.headers) : undefined
- };
- }
- // Convert thrown data() to ErrorResponse instances
- return {
- type: ResultType.error,
- error: new ErrorResponseImpl(((_result$init3 = result.init) == null ? void 0 : _result$init3.status) || 500, undefined, result.data),
- statusCode: isRouteErrorResponse(result) ? result.status : undefined,
- headers: (_result$init4 = result.init) != null && _result$init4.headers ? new Headers(result.init.headers) : undefined
- };
- }
- return {
- type: ResultType.error,
- error: result,
- statusCode: isRouteErrorResponse(result) ? result.status : undefined
- };
- }
- if (isDeferredData(result)) {
- var _result$init5, _result$init6;
- return {
- type: ResultType.deferred,
- deferredData: result,
- statusCode: (_result$init5 = result.init) == null ? void 0 : _result$init5.status,
- headers: ((_result$init6 = result.init) == null ? void 0 : _result$init6.headers) && new Headers(result.init.headers)
- };
- }
- if (isDataWithResponseInit(result)) {
- var _result$init7, _result$init8;
- return {
- type: ResultType.data,
- data: result.data,
- statusCode: (_result$init7 = result.init) == null ? void 0 : _result$init7.status,
- headers: (_result$init8 = result.init) != null && _result$init8.headers ? new Headers(result.init.headers) : undefined
- };
- }
- return {
- type: ResultType.data,
- data: result
- };
- }
- // Support relative routing in internal redirects
- function normalizeRelativeRoutingRedirectResponse(response, request, routeId, matches, basename, v7_relativeSplatPath) {
- let location = response.headers.get("Location");
- invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
- if (!ABSOLUTE_URL_REGEX.test(location)) {
- let trimmedMatches = matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1);
- location = normalizeTo(new URL(request.url), trimmedMatches, basename, true, location, v7_relativeSplatPath);
- response.headers.set("Location", location);
- }
- return response;
- }
- function normalizeRedirectLocation(location, currentUrl, basename) {
- if (ABSOLUTE_URL_REGEX.test(location)) {
- // Strip off the protocol+origin for same-origin + same-basename absolute redirects
- let normalizedLocation = location;
- let url = normalizedLocation.startsWith("//") ? new URL(currentUrl.protocol + normalizedLocation) : new URL(normalizedLocation);
- let isSameBasename = stripBasename(url.pathname, basename) != null;
- if (url.origin === currentUrl.origin && isSameBasename) {
- return url.pathname + url.search + url.hash;
- }
- }
- return location;
- }
- // Utility method for creating the Request instances for loaders/actions during
- // client-side navigations and fetches. During SSR we will always have a
- // Request instance from the static handler (query/queryRoute)
- function createClientSideRequest(history, location, signal, submission) {
- let url = history.createURL(stripHashFromPath(location)).toString();
- let init = {
- signal
- };
- if (submission && isMutationMethod(submission.formMethod)) {
- let {
- formMethod,
- formEncType
- } = submission;
- // Didn't think we needed this but it turns out unlike other methods, patch
- // won't be properly normalized to uppercase and results in a 405 error.
- // See: https://fetch.spec.whatwg.org/#concept-method
- init.method = formMethod.toUpperCase();
- if (formEncType === "application/json") {
- init.headers = new Headers({
- "Content-Type": formEncType
- });
- init.body = JSON.stringify(submission.json);
- } else if (formEncType === "text/plain") {
- // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
- init.body = submission.text;
- } else if (formEncType === "application/x-www-form-urlencoded" && submission.formData) {
- // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
- init.body = convertFormDataToSearchParams(submission.formData);
- } else {
- // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
- init.body = submission.formData;
- }
- }
- return new Request(url, init);
- }
- function convertFormDataToSearchParams(formData) {
- let searchParams = new URLSearchParams();
- for (let [key, value] of formData.entries()) {
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
- searchParams.append(key, typeof value === "string" ? value : value.name);
- }
- return searchParams;
- }
- function convertSearchParamsToFormData(searchParams) {
- let formData = new FormData();
- for (let [key, value] of searchParams.entries()) {
- formData.append(key, value);
- }
- return formData;
- }
- function processRouteLoaderData(matches, results, pendingActionResult, activeDeferreds, skipLoaderErrorBubbling) {
- // Fill in loaderData/errors from our loaders
- let loaderData = {};
- let errors = null;
- let statusCode;
- let foundError = false;
- let loaderHeaders = {};
- let pendingError = pendingActionResult && isErrorResult(pendingActionResult[1]) ? pendingActionResult[1].error : undefined;
- // Process loader results into state.loaderData/state.errors
- matches.forEach(match => {
- if (!(match.route.id in results)) {
- return;
- }
- let id = match.route.id;
- let result = results[id];
- invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData");
- if (isErrorResult(result)) {
- let error = result.error;
- // If we have a pending action error, we report it at the highest-route
- // that throws a loader error, and then clear it out to indicate that
- // it was consumed
- if (pendingError !== undefined) {
- error = pendingError;
- pendingError = undefined;
- }
- errors = errors || {};
- if (skipLoaderErrorBubbling) {
- errors[id] = error;
- } else {
- // Look upwards from the matched route for the closest ancestor error
- // boundary, defaulting to the root match. Prefer higher error values
- // if lower errors bubble to the same boundary
- let boundaryMatch = findNearestBoundary(matches, id);
- if (errors[boundaryMatch.route.id] == null) {
- errors[boundaryMatch.route.id] = error;
- }
- }
- // Clear our any prior loaderData for the throwing route
- loaderData[id] = undefined;
- // Once we find our first (highest) error, we set the status code and
- // prevent deeper status codes from overriding
- if (!foundError) {
- foundError = true;
- statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;
- }
- if (result.headers) {
- loaderHeaders[id] = result.headers;
- }
- } else {
- if (isDeferredResult(result)) {
- activeDeferreds.set(id, result.deferredData);
- loaderData[id] = result.deferredData.data;
- // Error status codes always override success status codes, but if all
- // loaders are successful we take the deepest status code.
- if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
- statusCode = result.statusCode;
- }
- if (result.headers) {
- loaderHeaders[id] = result.headers;
- }
- } else {
- loaderData[id] = result.data;
- // Error status codes always override success status codes, but if all
- // loaders are successful we take the deepest status code.
- if (result.statusCode && result.statusCode !== 200 && !foundError) {
- statusCode = result.statusCode;
- }
- if (result.headers) {
- loaderHeaders[id] = result.headers;
- }
- }
- }
- });
- // If we didn't consume the pending action error (i.e., all loaders
- // resolved), then consume it here. Also clear out any loaderData for the
- // throwing route
- if (pendingError !== undefined && pendingActionResult) {
- errors = {
- [pendingActionResult[0]]: pendingError
- };
- loaderData[pendingActionResult[0]] = undefined;
- }
- return {
- loaderData,
- errors,
- statusCode: statusCode || 200,
- loaderHeaders
- };
- }
- function processLoaderData(state, matches, results, pendingActionResult, revalidatingFetchers, fetcherResults, activeDeferreds) {
- let {
- loaderData,
- errors
- } = processRouteLoaderData(matches, results, pendingActionResult, activeDeferreds, false // This method is only called client side so we always want to bubble
- );
- // Process results from our revalidating fetchers
- revalidatingFetchers.forEach(rf => {
- let {
- key,
- match,
- controller
- } = rf;
- let result = fetcherResults[key];
- invariant(result, "Did not find corresponding fetcher result");
- // Process fetcher non-redirect errors
- if (controller && controller.signal.aborted) {
- // Nothing to do for aborted fetchers
- return;
- } else if (isErrorResult(result)) {
- let boundaryMatch = findNearestBoundary(state.matches, match == null ? void 0 : match.route.id);
- if (!(errors && errors[boundaryMatch.route.id])) {
- errors = _extends({}, errors, {
- [boundaryMatch.route.id]: result.error
- });
- }
- state.fetchers.delete(key);
- } else if (isRedirectResult(result)) {
- // Should never get here, redirects should get processed above, but we
- // keep this to type narrow to a success result in the else
- invariant(false, "Unhandled fetcher revalidation redirect");
- } else if (isDeferredResult(result)) {
- // Should never get here, deferred data should be awaited for fetchers
- // in resolveDeferredResults
- invariant(false, "Unhandled fetcher deferred data");
- } else {
- let doneFetcher = getDoneFetcher(result.data);
- state.fetchers.set(key, doneFetcher);
- }
- });
- return {
- loaderData,
- errors
- };
- }
- function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
- let mergedLoaderData = _extends({}, newLoaderData);
- for (let match of matches) {
- let id = match.route.id;
- if (newLoaderData.hasOwnProperty(id)) {
- if (newLoaderData[id] !== undefined) {
- mergedLoaderData[id] = newLoaderData[id];
- }
- } else if (loaderData[id] !== undefined && match.route.loader) {
- // Preserve existing keys not included in newLoaderData and where a loader
- // wasn't removed by HMR
- mergedLoaderData[id] = loaderData[id];
- }
- if (errors && errors.hasOwnProperty(id)) {
- // Don't keep any loader data below the boundary
- break;
- }
- }
- return mergedLoaderData;
- }
- function getActionDataForCommit(pendingActionResult) {
- if (!pendingActionResult) {
- return {};
- }
- return isErrorResult(pendingActionResult[1]) ? {
- // Clear out prior actionData on errors
- actionData: {}
- } : {
- actionData: {
- [pendingActionResult[0]]: pendingActionResult[1].data
- }
- };
- }
- // Find the nearest error boundary, looking upwards from the leaf route (or the
- // route specified by routeId) for the closest ancestor error boundary,
- // defaulting to the root match
- function findNearestBoundary(matches, routeId) {
- let eligibleMatches = routeId ? matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1) : [...matches];
- return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];
- }
- function getShortCircuitMatches(routes) {
- // Prefer a root layout route if present, otherwise shim in a route object
- let route = routes.length === 1 ? routes[0] : routes.find(r => r.index || !r.path || r.path === "/") || {
- id: "__shim-error-route__"
- };
- return {
- matches: [{
- params: {},
- pathname: "",
- pathnameBase: "",
- route
- }],
- route
- };
- }
- function getInternalRouterError(status, _temp5) {
- let {
- pathname,
- routeId,
- method,
- type,
- message
- } = _temp5 === void 0 ? {} : _temp5;
- let statusText = "Unknown Server Error";
- let errorMessage = "Unknown @remix-run/router error";
- if (status === 400) {
- statusText = "Bad Request";
- if (method && pathname && routeId) {
- errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
- } else if (type === "defer-action") {
- errorMessage = "defer() is not supported in actions";
- } else if (type === "invalid-body") {
- errorMessage = "Unable to encode submission body";
- }
- } else if (status === 403) {
- statusText = "Forbidden";
- errorMessage = "Route \"" + routeId + "\" does not match URL \"" + pathname + "\"";
- } else if (status === 404) {
- statusText = "Not Found";
- errorMessage = "No route matches URL \"" + pathname + "\"";
- } else if (status === 405) {
- statusText = "Method Not Allowed";
- if (method && pathname && routeId) {
- errorMessage = "You made a " + method.toUpperCase() + " request to \"" + pathname + "\" but " + ("did not provide an `action` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
- } else if (method) {
- errorMessage = "Invalid request method \"" + method.toUpperCase() + "\"";
- }
- }
- return new ErrorResponseImpl(status || 500, statusText, new Error(errorMessage), true);
- }
- // Find any returned redirect errors, starting from the lowest match
- function findRedirect(results) {
- let entries = Object.entries(results);
- for (let i = entries.length - 1; i >= 0; i--) {
- let [key, result] = entries[i];
- if (isRedirectResult(result)) {
- return {
- key,
- result
- };
- }
- }
- }
- function stripHashFromPath(path) {
- let parsedPath = typeof path === "string" ? parsePath(path) : path;
- return createPath(_extends({}, parsedPath, {
- hash: ""
- }));
- }
- function isHashChangeOnly(a, b) {
- if (a.pathname !== b.pathname || a.search !== b.search) {
- return false;
- }
- if (a.hash === "") {
- // /page -> /page#hash
- return b.hash !== "";
- } else if (a.hash === b.hash) {
- // /page#hash -> /page#hash
- return true;
- } else if (b.hash !== "") {
- // /page#hash -> /page#other
- return true;
- }
- // If the hash is removed the browser will re-perform a request to the server
- // /page#hash -> /page
- return false;
- }
- function isDataStrategyResult(result) {
- return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
- }
- function isRedirectDataStrategyResultResult(result) {
- return isResponse(result.result) && redirectStatusCodes.has(result.result.status);
- }
- function isDeferredResult(result) {
- return result.type === ResultType.deferred;
- }
- function isErrorResult(result) {
- return result.type === ResultType.error;
- }
- function isRedirectResult(result) {
- return (result && result.type) === ResultType.redirect;
- }
- function isDataWithResponseInit(value) {
- return typeof value === "object" && value != null && "type" in value && "data" in value && "init" in value && value.type === "DataWithResponseInit";
- }
- function isDeferredData(value) {
- let deferred = value;
- return deferred && typeof deferred === "object" && typeof deferred.data === "object" && typeof deferred.subscribe === "function" && typeof deferred.cancel === "function" && typeof deferred.resolveData === "function";
- }
- function isResponse(value) {
- return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined";
- }
- function isRedirectResponse(result) {
- if (!isResponse(result)) {
- return false;
- }
- let status = result.status;
- let location = result.headers.get("Location");
- return status >= 300 && status <= 399 && location != null;
- }
- function isValidMethod(method) {
- return validRequestMethods.has(method.toLowerCase());
- }
- function isMutationMethod(method) {
- return validMutationMethods.has(method.toLowerCase());
- }
- async function resolveNavigationDeferredResults(matches, results, signal, currentMatches, currentLoaderData) {
- let entries = Object.entries(results);
- for (let index = 0; index < entries.length; index++) {
- let [routeId, result] = entries[index];
- let match = matches.find(m => (m == null ? void 0 : m.route.id) === routeId);
- // If we don't have a match, then we can have a deferred result to do
- // anything with. This is for revalidating fetchers where the route was
- // removed during HMR
- if (!match) {
- continue;
- }
- let currentMatch = currentMatches.find(m => m.route.id === match.route.id);
- let isRevalidatingLoader = currentMatch != null && !isNewRouteInstance(currentMatch, match) && (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
- if (isDeferredResult(result) && isRevalidatingLoader) {
- // Note: we do not have to touch activeDeferreds here since we race them
- // against the signal in resolveDeferredData and they'll get aborted
- // there if needed
- await resolveDeferredData(result, signal, false).then(result => {
- if (result) {
- results[routeId] = result;
- }
- });
- }
- }
- }
- async function resolveFetcherDeferredResults(matches, results, revalidatingFetchers) {
- for (let index = 0; index < revalidatingFetchers.length; index++) {
- let {
- key,
- routeId,
- controller
- } = revalidatingFetchers[index];
- let result = results[key];
- let match = matches.find(m => (m == null ? void 0 : m.route.id) === routeId);
- // If we don't have a match, then we can have a deferred result to do
- // anything with. This is for revalidating fetchers where the route was
- // removed during HMR
- if (!match) {
- continue;
- }
- if (isDeferredResult(result)) {
- // Note: we do not have to touch activeDeferreds here since we race them
- // against the signal in resolveDeferredData and they'll get aborted
- // there if needed
- invariant(controller, "Expected an AbortController for revalidating fetcher deferred result");
- await resolveDeferredData(result, controller.signal, true).then(result => {
- if (result) {
- results[key] = result;
- }
- });
- }
- }
- }
- async function resolveDeferredData(result, signal, unwrap) {
- if (unwrap === void 0) {
- unwrap = false;
- }
- let aborted = await result.deferredData.resolveData(signal);
- if (aborted) {
- return;
- }
- if (unwrap) {
- try {
- return {
- type: ResultType.data,
- data: result.deferredData.unwrappedData
- };
- } catch (e) {
- // Handle any TrackedPromise._error values encountered while unwrapping
- return {
- type: ResultType.error,
- error: e
- };
- }
- }
- return {
- type: ResultType.data,
- data: result.deferredData.data
- };
- }
- function hasNakedIndexQuery(search) {
- return new URLSearchParams(search).getAll("index").some(v => v === "");
- }
- function getTargetMatch(matches, location) {
- let search = typeof location === "string" ? parsePath(location).search : location.search;
- if (matches[matches.length - 1].route.index && hasNakedIndexQuery(search || "")) {
- // Return the leaf index route when index is present
- return matches[matches.length - 1];
- }
- // Otherwise grab the deepest "path contributing" match (ignoring index and
- // pathless layout routes)
- let pathMatches = getPathContributingMatches(matches);
- return pathMatches[pathMatches.length - 1];
- }
- function getSubmissionFromNavigation(navigation) {
- let {
- formMethod,
- formAction,
- formEncType,
- text,
- formData,
- json
- } = navigation;
- if (!formMethod || !formAction || !formEncType) {
- return;
- }
- if (text != null) {
- return {
- formMethod,
- formAction,
- formEncType,
- formData: undefined,
- json: undefined,
- text
- };
- } else if (formData != null) {
- return {
- formMethod,
- formAction,
- formEncType,
- formData,
- json: undefined,
- text: undefined
- };
- } else if (json !== undefined) {
- return {
- formMethod,
- formAction,
- formEncType,
- formData: undefined,
- json,
- text: undefined
- };
- }
- }
- function getLoadingNavigation(location, submission) {
- if (submission) {
- let navigation = {
- state: "loading",
- location,
- formMethod: submission.formMethod,
- formAction: submission.formAction,
- formEncType: submission.formEncType,
- formData: submission.formData,
- json: submission.json,
- text: submission.text
- };
- return navigation;
- } else {
- let navigation = {
- state: "loading",
- location,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- json: undefined,
- text: undefined
- };
- return navigation;
- }
- }
- function getSubmittingNavigation(location, submission) {
- let navigation = {
- state: "submitting",
- location,
- formMethod: submission.formMethod,
- formAction: submission.formAction,
- formEncType: submission.formEncType,
- formData: submission.formData,
- json: submission.json,
- text: submission.text
- };
- return navigation;
- }
- function getLoadingFetcher(submission, data) {
- if (submission) {
- let fetcher = {
- state: "loading",
- formMethod: submission.formMethod,
- formAction: submission.formAction,
- formEncType: submission.formEncType,
- formData: submission.formData,
- json: submission.json,
- text: submission.text,
- data
- };
- return fetcher;
- } else {
- let fetcher = {
- state: "loading",
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- json: undefined,
- text: undefined,
- data
- };
- return fetcher;
- }
- }
- function getSubmittingFetcher(submission, existingFetcher) {
- let fetcher = {
- state: "submitting",
- formMethod: submission.formMethod,
- formAction: submission.formAction,
- formEncType: submission.formEncType,
- formData: submission.formData,
- json: submission.json,
- text: submission.text,
- data: existingFetcher ? existingFetcher.data : undefined
- };
- return fetcher;
- }
- function getDoneFetcher(data) {
- let fetcher = {
- state: "idle",
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- json: undefined,
- text: undefined,
- data
- };
- return fetcher;
- }
- function restoreAppliedTransitions(_window, transitions) {
- try {
- let sessionPositions = _window.sessionStorage.getItem(TRANSITIONS_STORAGE_KEY);
- if (sessionPositions) {
- let json = JSON.parse(sessionPositions);
- for (let [k, v] of Object.entries(json || {})) {
- if (v && Array.isArray(v)) {
- transitions.set(k, new Set(v || []));
- }
- }
- }
- } catch (e) {
- // no-op, use default empty object
- }
- }
- function persistAppliedTransitions(_window, transitions) {
- if (transitions.size > 0) {
- let json = {};
- for (let [k, v] of transitions) {
- json[k] = [...v];
- }
- try {
- _window.sessionStorage.setItem(TRANSITIONS_STORAGE_KEY, JSON.stringify(json));
- } catch (error) {
- warning(false, "Failed to save applied view transitions in sessionStorage (" + error + ").");
- }
- }
- }
- //#endregion
- export { AbortedDeferredError, Action, IDLE_BLOCKER, IDLE_FETCHER, IDLE_NAVIGATION, UNSAFE_DEFERRED_SYMBOL, DeferredData as UNSAFE_DeferredData, ErrorResponseImpl as UNSAFE_ErrorResponseImpl, convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch, convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes, decodePath as UNSAFE_decodePath, getResolveToMatches as UNSAFE_getResolveToMatches, invariant as UNSAFE_invariant, warning as UNSAFE_warning, createBrowserHistory, createHashHistory, createMemoryHistory, createPath, createRouter, createStaticHandler, data, defer, generatePath, getStaticContextFromError, getToPathname, isDataWithResponseInit, isDeferredData, isRouteErrorResponse, joinPaths, json, matchPath, matchRoutes, normalizePathname, parsePath, redirect, redirectDocument, replace, resolvePath, resolveTo, stripBasename };
- //# sourceMappingURL=router.js.map
|