router.ts 181 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031
  1. import type { History, Location, Path, To } from "./history";
  2. import {
  3. Action as HistoryAction,
  4. createLocation,
  5. createPath,
  6. invariant,
  7. parsePath,
  8. warning,
  9. } from "./history";
  10. import type {
  11. AgnosticDataRouteMatch,
  12. AgnosticDataRouteObject,
  13. DataStrategyMatch,
  14. AgnosticRouteObject,
  15. DataResult,
  16. DataStrategyFunction,
  17. DataStrategyFunctionArgs,
  18. DeferredData,
  19. DeferredResult,
  20. DetectErrorBoundaryFunction,
  21. ErrorResult,
  22. FormEncType,
  23. FormMethod,
  24. HTMLFormMethod,
  25. DataStrategyResult,
  26. ImmutableRouteKey,
  27. MapRoutePropertiesFunction,
  28. MutationFormMethod,
  29. RedirectResult,
  30. RouteData,
  31. RouteManifest,
  32. ShouldRevalidateFunctionArgs,
  33. Submission,
  34. SuccessResult,
  35. UIMatch,
  36. V7_FormMethod,
  37. V7_MutationFormMethod,
  38. AgnosticPatchRoutesOnNavigationFunction,
  39. DataWithResponseInit,
  40. } from "./utils";
  41. import {
  42. ErrorResponseImpl,
  43. ResultType,
  44. convertRouteMatchToUiMatch,
  45. convertRoutesToDataRoutes,
  46. getPathContributingMatches,
  47. getResolveToMatches,
  48. immutableRouteKeys,
  49. isRouteErrorResponse,
  50. joinPaths,
  51. matchRoutes,
  52. matchRoutesImpl,
  53. resolveTo,
  54. stripBasename,
  55. } from "./utils";
  56. ////////////////////////////////////////////////////////////////////////////////
  57. //#region Types and Constants
  58. ////////////////////////////////////////////////////////////////////////////////
  59. /**
  60. * A Router instance manages all navigation and data loading/mutations
  61. */
  62. export interface Router {
  63. /**
  64. * @internal
  65. * PRIVATE - DO NOT USE
  66. *
  67. * Return the basename for the router
  68. */
  69. get basename(): RouterInit["basename"];
  70. /**
  71. * @internal
  72. * PRIVATE - DO NOT USE
  73. *
  74. * Return the future config for the router
  75. */
  76. get future(): FutureConfig;
  77. /**
  78. * @internal
  79. * PRIVATE - DO NOT USE
  80. *
  81. * Return the current state of the router
  82. */
  83. get state(): RouterState;
  84. /**
  85. * @internal
  86. * PRIVATE - DO NOT USE
  87. *
  88. * Return the routes for this router instance
  89. */
  90. get routes(): AgnosticDataRouteObject[];
  91. /**
  92. * @internal
  93. * PRIVATE - DO NOT USE
  94. *
  95. * Return the window associated with the router
  96. */
  97. get window(): RouterInit["window"];
  98. /**
  99. * @internal
  100. * PRIVATE - DO NOT USE
  101. *
  102. * Initialize the router, including adding history listeners and kicking off
  103. * initial data fetches. Returns a function to cleanup listeners and abort
  104. * any in-progress loads
  105. */
  106. initialize(): Router;
  107. /**
  108. * @internal
  109. * PRIVATE - DO NOT USE
  110. *
  111. * Subscribe to router.state updates
  112. *
  113. * @param fn function to call with the new state
  114. */
  115. subscribe(fn: RouterSubscriber): () => void;
  116. /**
  117. * @internal
  118. * PRIVATE - DO NOT USE
  119. *
  120. * Enable scroll restoration behavior in the router
  121. *
  122. * @param savedScrollPositions Object that will manage positions, in case
  123. * it's being restored from sessionStorage
  124. * @param getScrollPosition Function to get the active Y scroll position
  125. * @param getKey Function to get the key to use for restoration
  126. */
  127. enableScrollRestoration(
  128. savedScrollPositions: Record<string, number>,
  129. getScrollPosition: GetScrollPositionFunction,
  130. getKey?: GetScrollRestorationKeyFunction
  131. ): () => void;
  132. /**
  133. * @internal
  134. * PRIVATE - DO NOT USE
  135. *
  136. * Navigate forward/backward in the history stack
  137. * @param to Delta to move in the history stack
  138. */
  139. navigate(to: number): Promise<void>;
  140. /**
  141. * Navigate to the given path
  142. * @param to Path to navigate to
  143. * @param opts Navigation options (method, submission, etc.)
  144. */
  145. navigate(to: To | null, opts?: RouterNavigateOptions): Promise<void>;
  146. /**
  147. * @internal
  148. * PRIVATE - DO NOT USE
  149. *
  150. * Trigger a fetcher load/submission
  151. *
  152. * @param key Fetcher key
  153. * @param routeId Route that owns the fetcher
  154. * @param href href to fetch
  155. * @param opts Fetcher options, (method, submission, etc.)
  156. */
  157. fetch(
  158. key: string,
  159. routeId: string,
  160. href: string | null,
  161. opts?: RouterFetchOptions
  162. ): void;
  163. /**
  164. * @internal
  165. * PRIVATE - DO NOT USE
  166. *
  167. * Trigger a revalidation of all current route loaders and fetcher loads
  168. */
  169. revalidate(): void;
  170. /**
  171. * @internal
  172. * PRIVATE - DO NOT USE
  173. *
  174. * Utility function to create an href for the given location
  175. * @param location
  176. */
  177. createHref(location: Location | URL): string;
  178. /**
  179. * @internal
  180. * PRIVATE - DO NOT USE
  181. *
  182. * Utility function to URL encode a destination path according to the internal
  183. * history implementation
  184. * @param to
  185. */
  186. encodeLocation(to: To): Path;
  187. /**
  188. * @internal
  189. * PRIVATE - DO NOT USE
  190. *
  191. * Get/create a fetcher for the given key
  192. * @param key
  193. */
  194. getFetcher<TData = any>(key: string): Fetcher<TData>;
  195. /**
  196. * @internal
  197. * PRIVATE - DO NOT USE
  198. *
  199. * Delete the fetcher for a given key
  200. * @param key
  201. */
  202. deleteFetcher(key: string): void;
  203. /**
  204. * @internal
  205. * PRIVATE - DO NOT USE
  206. *
  207. * Cleanup listeners and abort any in-progress loads
  208. */
  209. dispose(): void;
  210. /**
  211. * @internal
  212. * PRIVATE - DO NOT USE
  213. *
  214. * Get a navigation blocker
  215. * @param key The identifier for the blocker
  216. * @param fn The blocker function implementation
  217. */
  218. getBlocker(key: string, fn: BlockerFunction): Blocker;
  219. /**
  220. * @internal
  221. * PRIVATE - DO NOT USE
  222. *
  223. * Delete a navigation blocker
  224. * @param key The identifier for the blocker
  225. */
  226. deleteBlocker(key: string): void;
  227. /**
  228. * @internal
  229. * PRIVATE DO NOT USE
  230. *
  231. * Patch additional children routes into an existing parent route
  232. * @param routeId The parent route id or a callback function accepting `patch`
  233. * to perform batch patching
  234. * @param children The additional children routes
  235. */
  236. patchRoutes(routeId: string | null, children: AgnosticRouteObject[]): void;
  237. /**
  238. * @internal
  239. * PRIVATE - DO NOT USE
  240. *
  241. * HMR needs to pass in-flight route updates to React Router
  242. * TODO: Replace this with granular route update APIs (addRoute, updateRoute, deleteRoute)
  243. */
  244. _internalSetRoutes(routes: AgnosticRouteObject[]): void;
  245. /**
  246. * @internal
  247. * PRIVATE - DO NOT USE
  248. *
  249. * Internal fetch AbortControllers accessed by unit tests
  250. */
  251. _internalFetchControllers: Map<string, AbortController>;
  252. /**
  253. * @internal
  254. * PRIVATE - DO NOT USE
  255. *
  256. * Internal pending DeferredData instances accessed by unit tests
  257. */
  258. _internalActiveDeferreds: Map<string, DeferredData>;
  259. }
  260. /**
  261. * State maintained internally by the router. During a navigation, all states
  262. * reflect the the "old" location unless otherwise noted.
  263. */
  264. export interface RouterState {
  265. /**
  266. * The action of the most recent navigation
  267. */
  268. historyAction: HistoryAction;
  269. /**
  270. * The current location reflected by the router
  271. */
  272. location: Location;
  273. /**
  274. * The current set of route matches
  275. */
  276. matches: AgnosticDataRouteMatch[];
  277. /**
  278. * Tracks whether we've completed our initial data load
  279. */
  280. initialized: boolean;
  281. /**
  282. * Current scroll position we should start at for a new view
  283. * - number -> scroll position to restore to
  284. * - false -> do not restore scroll at all (used during submissions)
  285. * - null -> don't have a saved position, scroll to hash or top of page
  286. */
  287. restoreScrollPosition: number | false | null;
  288. /**
  289. * Indicate whether this navigation should skip resetting the scroll position
  290. * if we are unable to restore the scroll position
  291. */
  292. preventScrollReset: boolean;
  293. /**
  294. * Tracks the state of the current navigation
  295. */
  296. navigation: Navigation;
  297. /**
  298. * Tracks any in-progress revalidations
  299. */
  300. revalidation: RevalidationState;
  301. /**
  302. * Data from the loaders for the current matches
  303. */
  304. loaderData: RouteData;
  305. /**
  306. * Data from the action for the current matches
  307. */
  308. actionData: RouteData | null;
  309. /**
  310. * Errors caught from loaders for the current matches
  311. */
  312. errors: RouteData | null;
  313. /**
  314. * Map of current fetchers
  315. */
  316. fetchers: Map<string, Fetcher>;
  317. /**
  318. * Map of current blockers
  319. */
  320. blockers: Map<string, Blocker>;
  321. }
  322. /**
  323. * Data that can be passed into hydrate a Router from SSR
  324. */
  325. export type HydrationState = Partial<
  326. Pick<RouterState, "loaderData" | "actionData" | "errors">
  327. >;
  328. /**
  329. * Future flags to toggle new feature behavior
  330. */
  331. export interface FutureConfig {
  332. v7_fetcherPersist: boolean;
  333. v7_normalizeFormMethod: boolean;
  334. v7_partialHydration: boolean;
  335. v7_prependBasename: boolean;
  336. v7_relativeSplatPath: boolean;
  337. v7_skipActionErrorRevalidation: boolean;
  338. }
  339. /**
  340. * Initialization options for createRouter
  341. */
  342. export interface RouterInit {
  343. routes: AgnosticRouteObject[];
  344. history: History;
  345. basename?: string;
  346. /**
  347. * @deprecated Use `mapRouteProperties` instead
  348. */
  349. detectErrorBoundary?: DetectErrorBoundaryFunction;
  350. mapRouteProperties?: MapRoutePropertiesFunction;
  351. future?: Partial<FutureConfig>;
  352. hydrationData?: HydrationState;
  353. window?: Window;
  354. dataStrategy?: DataStrategyFunction;
  355. patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction;
  356. }
  357. /**
  358. * State returned from a server-side query() call
  359. */
  360. export interface StaticHandlerContext {
  361. basename: Router["basename"];
  362. location: RouterState["location"];
  363. matches: RouterState["matches"];
  364. loaderData: RouterState["loaderData"];
  365. actionData: RouterState["actionData"];
  366. errors: RouterState["errors"];
  367. statusCode: number;
  368. loaderHeaders: Record<string, Headers>;
  369. actionHeaders: Record<string, Headers>;
  370. activeDeferreds: Record<string, DeferredData> | null;
  371. _deepestRenderedBoundaryId?: string | null;
  372. }
  373. /**
  374. * A StaticHandler instance manages a singular SSR navigation/fetch event
  375. */
  376. export interface StaticHandler {
  377. dataRoutes: AgnosticDataRouteObject[];
  378. query(
  379. request: Request,
  380. opts?: {
  381. requestContext?: unknown;
  382. skipLoaderErrorBubbling?: boolean;
  383. dataStrategy?: DataStrategyFunction;
  384. }
  385. ): Promise<StaticHandlerContext | Response>;
  386. queryRoute(
  387. request: Request,
  388. opts?: {
  389. routeId?: string;
  390. requestContext?: unknown;
  391. dataStrategy?: DataStrategyFunction;
  392. }
  393. ): Promise<any>;
  394. }
  395. type ViewTransitionOpts = {
  396. currentLocation: Location;
  397. nextLocation: Location;
  398. };
  399. /**
  400. * Subscriber function signature for changes to router state
  401. */
  402. export interface RouterSubscriber {
  403. (
  404. state: RouterState,
  405. opts: {
  406. deletedFetchers: string[];
  407. viewTransitionOpts?: ViewTransitionOpts;
  408. flushSync: boolean;
  409. }
  410. ): void;
  411. }
  412. /**
  413. * Function signature for determining the key to be used in scroll restoration
  414. * for a given location
  415. */
  416. export interface GetScrollRestorationKeyFunction {
  417. (location: Location, matches: UIMatch[]): string | null;
  418. }
  419. /**
  420. * Function signature for determining the current scroll position
  421. */
  422. export interface GetScrollPositionFunction {
  423. (): number;
  424. }
  425. export type RelativeRoutingType = "route" | "path";
  426. // Allowed for any navigation or fetch
  427. type BaseNavigateOrFetchOptions = {
  428. preventScrollReset?: boolean;
  429. relative?: RelativeRoutingType;
  430. flushSync?: boolean;
  431. };
  432. // Only allowed for navigations
  433. type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
  434. replace?: boolean;
  435. state?: any;
  436. fromRouteId?: string;
  437. viewTransition?: boolean;
  438. };
  439. // Only allowed for submission navigations
  440. type BaseSubmissionOptions = {
  441. formMethod?: HTMLFormMethod;
  442. formEncType?: FormEncType;
  443. } & (
  444. | { formData: FormData; body?: undefined }
  445. | { formData?: undefined; body: any }
  446. );
  447. /**
  448. * Options for a navigate() call for a normal (non-submission) navigation
  449. */
  450. type LinkNavigateOptions = BaseNavigateOptions;
  451. /**
  452. * Options for a navigate() call for a submission navigation
  453. */
  454. type SubmissionNavigateOptions = BaseNavigateOptions & BaseSubmissionOptions;
  455. /**
  456. * Options to pass to navigate() for a navigation
  457. */
  458. export type RouterNavigateOptions =
  459. | LinkNavigateOptions
  460. | SubmissionNavigateOptions;
  461. /**
  462. * Options for a fetch() load
  463. */
  464. type LoadFetchOptions = BaseNavigateOrFetchOptions;
  465. /**
  466. * Options for a fetch() submission
  467. */
  468. type SubmitFetchOptions = BaseNavigateOrFetchOptions & BaseSubmissionOptions;
  469. /**
  470. * Options to pass to fetch()
  471. */
  472. export type RouterFetchOptions = LoadFetchOptions | SubmitFetchOptions;
  473. /**
  474. * Potential states for state.navigation
  475. */
  476. export type NavigationStates = {
  477. Idle: {
  478. state: "idle";
  479. location: undefined;
  480. formMethod: undefined;
  481. formAction: undefined;
  482. formEncType: undefined;
  483. formData: undefined;
  484. json: undefined;
  485. text: undefined;
  486. };
  487. Loading: {
  488. state: "loading";
  489. location: Location;
  490. formMethod: Submission["formMethod"] | undefined;
  491. formAction: Submission["formAction"] | undefined;
  492. formEncType: Submission["formEncType"] | undefined;
  493. formData: Submission["formData"] | undefined;
  494. json: Submission["json"] | undefined;
  495. text: Submission["text"] | undefined;
  496. };
  497. Submitting: {
  498. state: "submitting";
  499. location: Location;
  500. formMethod: Submission["formMethod"];
  501. formAction: Submission["formAction"];
  502. formEncType: Submission["formEncType"];
  503. formData: Submission["formData"];
  504. json: Submission["json"];
  505. text: Submission["text"];
  506. };
  507. };
  508. export type Navigation = NavigationStates[keyof NavigationStates];
  509. export type RevalidationState = "idle" | "loading";
  510. /**
  511. * Potential states for fetchers
  512. */
  513. type FetcherStates<TData = any> = {
  514. Idle: {
  515. state: "idle";
  516. formMethod: undefined;
  517. formAction: undefined;
  518. formEncType: undefined;
  519. text: undefined;
  520. formData: undefined;
  521. json: undefined;
  522. data: TData | undefined;
  523. };
  524. Loading: {
  525. state: "loading";
  526. formMethod: Submission["formMethod"] | undefined;
  527. formAction: Submission["formAction"] | undefined;
  528. formEncType: Submission["formEncType"] | undefined;
  529. text: Submission["text"] | undefined;
  530. formData: Submission["formData"] | undefined;
  531. json: Submission["json"] | undefined;
  532. data: TData | undefined;
  533. };
  534. Submitting: {
  535. state: "submitting";
  536. formMethod: Submission["formMethod"];
  537. formAction: Submission["formAction"];
  538. formEncType: Submission["formEncType"];
  539. text: Submission["text"];
  540. formData: Submission["formData"];
  541. json: Submission["json"];
  542. data: TData | undefined;
  543. };
  544. };
  545. export type Fetcher<TData = any> =
  546. FetcherStates<TData>[keyof FetcherStates<TData>];
  547. interface BlockerBlocked {
  548. state: "blocked";
  549. reset(): void;
  550. proceed(): void;
  551. location: Location;
  552. }
  553. interface BlockerUnblocked {
  554. state: "unblocked";
  555. reset: undefined;
  556. proceed: undefined;
  557. location: undefined;
  558. }
  559. interface BlockerProceeding {
  560. state: "proceeding";
  561. reset: undefined;
  562. proceed: undefined;
  563. location: Location;
  564. }
  565. export type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
  566. export type BlockerFunction = (args: {
  567. currentLocation: Location;
  568. nextLocation: Location;
  569. historyAction: HistoryAction;
  570. }) => boolean;
  571. interface ShortCircuitable {
  572. /**
  573. * startNavigation does not need to complete the navigation because we
  574. * redirected or got interrupted
  575. */
  576. shortCircuited?: boolean;
  577. }
  578. type PendingActionResult = [string, SuccessResult | ErrorResult];
  579. interface HandleActionResult extends ShortCircuitable {
  580. /**
  581. * Route matches which may have been updated from fog of war discovery
  582. */
  583. matches?: RouterState["matches"];
  584. /**
  585. * Tuple for the returned or thrown value from the current action. The routeId
  586. * is the action route for success and the bubbled boundary route for errors.
  587. */
  588. pendingActionResult?: PendingActionResult;
  589. }
  590. interface HandleLoadersResult extends ShortCircuitable {
  591. /**
  592. * Route matches which may have been updated from fog of war discovery
  593. */
  594. matches?: RouterState["matches"];
  595. /**
  596. * loaderData returned from the current set of loaders
  597. */
  598. loaderData?: RouterState["loaderData"];
  599. /**
  600. * errors thrown from the current set of loaders
  601. */
  602. errors?: RouterState["errors"];
  603. }
  604. /**
  605. * Cached info for active fetcher.load() instances so they can participate
  606. * in revalidation
  607. */
  608. interface FetchLoadMatch {
  609. routeId: string;
  610. path: string;
  611. }
  612. /**
  613. * Identified fetcher.load() calls that need to be revalidated
  614. */
  615. interface RevalidatingFetcher extends FetchLoadMatch {
  616. key: string;
  617. match: AgnosticDataRouteMatch | null;
  618. matches: AgnosticDataRouteMatch[] | null;
  619. controller: AbortController | null;
  620. }
  621. const validMutationMethodsArr: MutationFormMethod[] = [
  622. "post",
  623. "put",
  624. "patch",
  625. "delete",
  626. ];
  627. const validMutationMethods = new Set<MutationFormMethod>(
  628. validMutationMethodsArr
  629. );
  630. const validRequestMethodsArr: FormMethod[] = [
  631. "get",
  632. ...validMutationMethodsArr,
  633. ];
  634. const validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);
  635. const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
  636. const redirectPreserveMethodStatusCodes = new Set([307, 308]);
  637. export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
  638. state: "idle",
  639. location: undefined,
  640. formMethod: undefined,
  641. formAction: undefined,
  642. formEncType: undefined,
  643. formData: undefined,
  644. json: undefined,
  645. text: undefined,
  646. };
  647. export const IDLE_FETCHER: FetcherStates["Idle"] = {
  648. state: "idle",
  649. data: undefined,
  650. formMethod: undefined,
  651. formAction: undefined,
  652. formEncType: undefined,
  653. formData: undefined,
  654. json: undefined,
  655. text: undefined,
  656. };
  657. export const IDLE_BLOCKER: BlockerUnblocked = {
  658. state: "unblocked",
  659. proceed: undefined,
  660. reset: undefined,
  661. location: undefined,
  662. };
  663. const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
  664. const defaultMapRouteProperties: MapRoutePropertiesFunction = (route) => ({
  665. hasErrorBoundary: Boolean(route.hasErrorBoundary),
  666. });
  667. const TRANSITIONS_STORAGE_KEY = "remix-router-transitions";
  668. //#endregion
  669. ////////////////////////////////////////////////////////////////////////////////
  670. //#region createRouter
  671. ////////////////////////////////////////////////////////////////////////////////
  672. /**
  673. * Create a router and listen to history POP navigations
  674. */
  675. export function createRouter(init: RouterInit): Router {
  676. const routerWindow = init.window
  677. ? init.window
  678. : typeof window !== "undefined"
  679. ? window
  680. : undefined;
  681. const isBrowser =
  682. typeof routerWindow !== "undefined" &&
  683. typeof routerWindow.document !== "undefined" &&
  684. typeof routerWindow.document.createElement !== "undefined";
  685. const isServer = !isBrowser;
  686. invariant(
  687. init.routes.length > 0,
  688. "You must provide a non-empty routes array to createRouter"
  689. );
  690. let mapRouteProperties: MapRoutePropertiesFunction;
  691. if (init.mapRouteProperties) {
  692. mapRouteProperties = init.mapRouteProperties;
  693. } else if (init.detectErrorBoundary) {
  694. // If they are still using the deprecated version, wrap it with the new API
  695. let detectErrorBoundary = init.detectErrorBoundary;
  696. mapRouteProperties = (route) => ({
  697. hasErrorBoundary: detectErrorBoundary(route),
  698. });
  699. } else {
  700. mapRouteProperties = defaultMapRouteProperties;
  701. }
  702. // Routes keyed by ID
  703. let manifest: RouteManifest = {};
  704. // Routes in tree format for matching
  705. let dataRoutes = convertRoutesToDataRoutes(
  706. init.routes,
  707. mapRouteProperties,
  708. undefined,
  709. manifest
  710. );
  711. let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
  712. let basename = init.basename || "/";
  713. let dataStrategyImpl = init.dataStrategy || defaultDataStrategy;
  714. let patchRoutesOnNavigationImpl = init.patchRoutesOnNavigation;
  715. // Config driven behavior flags
  716. let future: FutureConfig = {
  717. v7_fetcherPersist: false,
  718. v7_normalizeFormMethod: false,
  719. v7_partialHydration: false,
  720. v7_prependBasename: false,
  721. v7_relativeSplatPath: false,
  722. v7_skipActionErrorRevalidation: false,
  723. ...init.future,
  724. };
  725. // Cleanup function for history
  726. let unlistenHistory: (() => void) | null = null;
  727. // Externally-provided functions to call on all state changes
  728. let subscribers = new Set<RouterSubscriber>();
  729. // Externally-provided object to hold scroll restoration locations during routing
  730. let savedScrollPositions: Record<string, number> | null = null;
  731. // Externally-provided function to get scroll restoration keys
  732. let getScrollRestorationKey: GetScrollRestorationKeyFunction | null = null;
  733. // Externally-provided function to get current scroll position
  734. let getScrollPosition: GetScrollPositionFunction | null = null;
  735. // One-time flag to control the initial hydration scroll restoration. Because
  736. // we don't get the saved positions from <ScrollRestoration /> until _after_
  737. // the initial render, we need to manually trigger a separate updateState to
  738. // send along the restoreScrollPosition
  739. // Set to true if we have `hydrationData` since we assume we were SSR'd and that
  740. // SSR did the initial scroll restoration.
  741. let initialScrollRestored = init.hydrationData != null;
  742. let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
  743. let initialMatchesIsFOW = false;
  744. let initialErrors: RouteData | null = null;
  745. if (initialMatches == null && !patchRoutesOnNavigationImpl) {
  746. // If we do not match a user-provided-route, fall back to the root
  747. // to allow the error boundary to take over
  748. let error = getInternalRouterError(404, {
  749. pathname: init.history.location.pathname,
  750. });
  751. let { matches, route } = getShortCircuitMatches(dataRoutes);
  752. initialMatches = matches;
  753. initialErrors = { [route.id]: error };
  754. }
  755. // In SPA apps, if the user provided a patchRoutesOnNavigation implementation and
  756. // our initial match is a splat route, clear them out so we run through lazy
  757. // discovery on hydration in case there's a more accurate lazy route match.
  758. // In SSR apps (with `hydrationData`), we expect that the server will send
  759. // up the proper matched routes so we don't want to run lazy discovery on
  760. // initial hydration and want to hydrate into the splat route.
  761. if (initialMatches && !init.hydrationData) {
  762. let fogOfWar = checkFogOfWar(
  763. initialMatches,
  764. dataRoutes,
  765. init.history.location.pathname
  766. );
  767. if (fogOfWar.active) {
  768. initialMatches = null;
  769. }
  770. }
  771. let initialized: boolean;
  772. if (!initialMatches) {
  773. initialized = false;
  774. initialMatches = [];
  775. // If partial hydration and fog of war is enabled, we will be running
  776. // `patchRoutesOnNavigation` during hydration so include any partial matches as
  777. // the initial matches so we can properly render `HydrateFallback`'s
  778. if (future.v7_partialHydration) {
  779. let fogOfWar = checkFogOfWar(
  780. null,
  781. dataRoutes,
  782. init.history.location.pathname
  783. );
  784. if (fogOfWar.active && fogOfWar.matches) {
  785. initialMatchesIsFOW = true;
  786. initialMatches = fogOfWar.matches;
  787. }
  788. }
  789. } else if (initialMatches.some((m) => m.route.lazy)) {
  790. // All initialMatches need to be loaded before we're ready. If we have lazy
  791. // functions around still then we'll need to run them in initialize()
  792. initialized = false;
  793. } else if (!initialMatches.some((m) => m.route.loader)) {
  794. // If we've got no loaders to run, then we're good to go
  795. initialized = true;
  796. } else if (future.v7_partialHydration) {
  797. // If partial hydration is enabled, we're initialized so long as we were
  798. // provided with hydrationData for every route with a loader, and no loaders
  799. // were marked for explicit hydration
  800. let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
  801. let errors = init.hydrationData ? init.hydrationData.errors : null;
  802. // If errors exist, don't consider routes below the boundary
  803. if (errors) {
  804. let idx = initialMatches.findIndex(
  805. (m) => errors![m.route.id] !== undefined
  806. );
  807. initialized = initialMatches
  808. .slice(0, idx + 1)
  809. .every((m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors));
  810. } else {
  811. initialized = initialMatches.every(
  812. (m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors)
  813. );
  814. }
  815. } else {
  816. // Without partial hydration - we're initialized if we were provided any
  817. // hydrationData - which is expected to be complete
  818. initialized = init.hydrationData != null;
  819. }
  820. let router: Router;
  821. let state: RouterState = {
  822. historyAction: init.history.action,
  823. location: init.history.location,
  824. matches: initialMatches,
  825. initialized,
  826. navigation: IDLE_NAVIGATION,
  827. // Don't restore on initial updateState() if we were SSR'd
  828. restoreScrollPosition: init.hydrationData != null ? false : null,
  829. preventScrollReset: false,
  830. revalidation: "idle",
  831. loaderData: (init.hydrationData && init.hydrationData.loaderData) || {},
  832. actionData: (init.hydrationData && init.hydrationData.actionData) || null,
  833. errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
  834. fetchers: new Map(),
  835. blockers: new Map(),
  836. };
  837. // -- Stateful internal variables to manage navigations --
  838. // Current navigation in progress (to be committed in completeNavigation)
  839. let pendingAction: HistoryAction = HistoryAction.Pop;
  840. // Should the current navigation prevent the scroll reset if scroll cannot
  841. // be restored?
  842. let pendingPreventScrollReset = false;
  843. // AbortController for the active navigation
  844. let pendingNavigationController: AbortController | null;
  845. // Should the current navigation enable document.startViewTransition?
  846. let pendingViewTransitionEnabled = false;
  847. // Store applied view transitions so we can apply them on POP
  848. let appliedViewTransitions: Map<string, Set<string>> = new Map<
  849. string,
  850. Set<string>
  851. >();
  852. // Cleanup function for persisting applied transitions to sessionStorage
  853. let removePageHideEventListener: (() => void) | null = null;
  854. // We use this to avoid touching history in completeNavigation if a
  855. // revalidation is entirely uninterrupted
  856. let isUninterruptedRevalidation = false;
  857. // Use this internal flag to force revalidation of all loaders:
  858. // - submissions (completed or interrupted)
  859. // - useRevalidator()
  860. // - X-Remix-Revalidate (from redirect)
  861. let isRevalidationRequired = false;
  862. // Use this internal array to capture routes that require revalidation due
  863. // to a cancelled deferred on action submission
  864. let cancelledDeferredRoutes: string[] = [];
  865. // Use this internal array to capture fetcher loads that were cancelled by an
  866. // action navigation and require revalidation
  867. let cancelledFetcherLoads: Set<string> = new Set();
  868. // AbortControllers for any in-flight fetchers
  869. let fetchControllers = new Map<string, AbortController>();
  870. // Track loads based on the order in which they started
  871. let incrementingLoadId = 0;
  872. // Track the outstanding pending navigation data load to be compared against
  873. // the globally incrementing load when a fetcher load lands after a completed
  874. // navigation
  875. let pendingNavigationLoadId = -1;
  876. // Fetchers that triggered data reloads as a result of their actions
  877. let fetchReloadIds = new Map<string, number>();
  878. // Fetchers that triggered redirect navigations
  879. let fetchRedirectIds = new Set<string>();
  880. // Most recent href/match for fetcher.load calls for fetchers
  881. let fetchLoadMatches = new Map<string, FetchLoadMatch>();
  882. // Ref-count mounted fetchers so we know when it's ok to clean them up
  883. let activeFetchers = new Map<string, number>();
  884. // Fetchers that have requested a delete when using v7_fetcherPersist,
  885. // they'll be officially removed after they return to idle
  886. let deletedFetchers = new Set<string>();
  887. // Store DeferredData instances for active route matches. When a
  888. // route loader returns defer() we stick one in here. Then, when a nested
  889. // promise resolves we update loaderData. If a new navigation starts we
  890. // cancel active deferreds for eliminated routes.
  891. let activeDeferreds = new Map<string, DeferredData>();
  892. // Store blocker functions in a separate Map outside of router state since
  893. // we don't need to update UI state if they change
  894. let blockerFunctions = new Map<string, BlockerFunction>();
  895. // Map of pending patchRoutesOnNavigation() promises (keyed by path/matches) so
  896. // that we only kick them off once for a given combo
  897. let pendingPatchRoutes = new Map<
  898. string,
  899. ReturnType<AgnosticPatchRoutesOnNavigationFunction>
  900. >();
  901. // Flag to ignore the next history update, so we can revert the URL change on
  902. // a POP navigation that was blocked by the user without touching router state
  903. let unblockBlockerHistoryUpdate: (() => void) | undefined = undefined;
  904. // Initialize the router, all side effects should be kicked off from here.
  905. // Implemented as a Fluent API for ease of:
  906. // let router = createRouter(init).initialize();
  907. function initialize() {
  908. // If history informs us of a POP navigation, start the navigation but do not update
  909. // state. We'll update our own state once the navigation completes
  910. unlistenHistory = init.history.listen(
  911. ({ action: historyAction, location, delta }) => {
  912. // Ignore this event if it was just us resetting the URL from a
  913. // blocked POP navigation
  914. if (unblockBlockerHistoryUpdate) {
  915. unblockBlockerHistoryUpdate();
  916. unblockBlockerHistoryUpdate = undefined;
  917. return;
  918. }
  919. warning(
  920. blockerFunctions.size === 0 || delta != null,
  921. "You are trying to use a blocker on a POP navigation to a location " +
  922. "that was not created by @remix-run/router. This will fail silently in " +
  923. "production. This can happen if you are navigating outside the router " +
  924. "via `window.history.pushState`/`window.location.hash` instead of using " +
  925. "router navigation APIs. This can also happen if you are using " +
  926. "createHashRouter and the user manually changes the URL."
  927. );
  928. let blockerKey = shouldBlockNavigation({
  929. currentLocation: state.location,
  930. nextLocation: location,
  931. historyAction,
  932. });
  933. if (blockerKey && delta != null) {
  934. // Restore the URL to match the current UI, but don't update router state
  935. let nextHistoryUpdatePromise = new Promise<void>((resolve) => {
  936. unblockBlockerHistoryUpdate = resolve;
  937. });
  938. init.history.go(delta * -1);
  939. // Put the blocker into a blocked state
  940. updateBlocker(blockerKey, {
  941. state: "blocked",
  942. location,
  943. proceed() {
  944. updateBlocker(blockerKey!, {
  945. state: "proceeding",
  946. proceed: undefined,
  947. reset: undefined,
  948. location,
  949. });
  950. // Re-do the same POP navigation we just blocked, after the url
  951. // restoration is also complete. See:
  952. // https://github.com/remix-run/react-router/issues/11613
  953. nextHistoryUpdatePromise.then(() => init.history.go(delta));
  954. },
  955. reset() {
  956. let blockers = new Map(state.blockers);
  957. blockers.set(blockerKey!, IDLE_BLOCKER);
  958. updateState({ blockers });
  959. },
  960. });
  961. return;
  962. }
  963. return startNavigation(historyAction, location);
  964. }
  965. );
  966. if (isBrowser) {
  967. // FIXME: This feels gross. How can we cleanup the lines between
  968. // scrollRestoration/appliedTransitions persistance?
  969. restoreAppliedTransitions(routerWindow, appliedViewTransitions);
  970. let _saveAppliedTransitions = () =>
  971. persistAppliedTransitions(routerWindow, appliedViewTransitions);
  972. routerWindow.addEventListener("pagehide", _saveAppliedTransitions);
  973. removePageHideEventListener = () =>
  974. routerWindow.removeEventListener("pagehide", _saveAppliedTransitions);
  975. }
  976. // Kick off initial data load if needed. Use Pop to avoid modifying history
  977. // Note we don't do any handling of lazy here. For SPA's it'll get handled
  978. // in the normal navigation flow. For SSR it's expected that lazy modules are
  979. // resolved prior to router creation since we can't go into a fallbackElement
  980. // UI for SSR'd apps
  981. if (!state.initialized) {
  982. startNavigation(HistoryAction.Pop, state.location, {
  983. initialHydration: true,
  984. });
  985. }
  986. return router;
  987. }
  988. // Clean up a router and it's side effects
  989. function dispose() {
  990. if (unlistenHistory) {
  991. unlistenHistory();
  992. }
  993. if (removePageHideEventListener) {
  994. removePageHideEventListener();
  995. }
  996. subscribers.clear();
  997. pendingNavigationController && pendingNavigationController.abort();
  998. state.fetchers.forEach((_, key) => deleteFetcher(key));
  999. state.blockers.forEach((_, key) => deleteBlocker(key));
  1000. }
  1001. // Subscribe to state updates for the router
  1002. function subscribe(fn: RouterSubscriber) {
  1003. subscribers.add(fn);
  1004. return () => subscribers.delete(fn);
  1005. }
  1006. // Update our state and notify the calling context of the change
  1007. function updateState(
  1008. newState: Partial<RouterState>,
  1009. opts: {
  1010. flushSync?: boolean;
  1011. viewTransitionOpts?: ViewTransitionOpts;
  1012. } = {}
  1013. ): void {
  1014. state = {
  1015. ...state,
  1016. ...newState,
  1017. };
  1018. // Prep fetcher cleanup so we can tell the UI which fetcher data entries
  1019. // can be removed
  1020. let completedFetchers: string[] = [];
  1021. let deletedFetchersKeys: string[] = [];
  1022. if (future.v7_fetcherPersist) {
  1023. state.fetchers.forEach((fetcher, key) => {
  1024. if (fetcher.state === "idle") {
  1025. if (deletedFetchers.has(key)) {
  1026. // Unmounted from the UI and can be totally removed
  1027. deletedFetchersKeys.push(key);
  1028. } else {
  1029. // Returned to idle but still mounted in the UI, so semi-remains for
  1030. // revalidations and such
  1031. completedFetchers.push(key);
  1032. }
  1033. }
  1034. });
  1035. }
  1036. // Remove any lingering deleted fetchers that have already been removed
  1037. // from state.fetchers
  1038. deletedFetchers.forEach((key) => {
  1039. if (!state.fetchers.has(key) && !fetchControllers.has(key)) {
  1040. deletedFetchersKeys.push(key);
  1041. }
  1042. });
  1043. // Iterate over a local copy so that if flushSync is used and we end up
  1044. // removing and adding a new subscriber due to the useCallback dependencies,
  1045. // we don't get ourselves into a loop calling the new subscriber immediately
  1046. [...subscribers].forEach((subscriber) =>
  1047. subscriber(state, {
  1048. deletedFetchers: deletedFetchersKeys,
  1049. viewTransitionOpts: opts.viewTransitionOpts,
  1050. flushSync: opts.flushSync === true,
  1051. })
  1052. );
  1053. // Remove idle fetchers from state since we only care about in-flight fetchers.
  1054. if (future.v7_fetcherPersist) {
  1055. completedFetchers.forEach((key) => state.fetchers.delete(key));
  1056. deletedFetchersKeys.forEach((key) => deleteFetcher(key));
  1057. } else {
  1058. // We already called deleteFetcher() on these, can remove them from this
  1059. // Set now that we've handed the keys off to the data layer
  1060. deletedFetchersKeys.forEach((key) => deletedFetchers.delete(key));
  1061. }
  1062. }
  1063. // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
  1064. // and setting state.[historyAction/location/matches] to the new route.
  1065. // - Location is a required param
  1066. // - Navigation will always be set to IDLE_NAVIGATION
  1067. // - Can pass any other state in newState
  1068. function completeNavigation(
  1069. location: Location,
  1070. newState: Partial<Omit<RouterState, "action" | "location" | "navigation">>,
  1071. { flushSync }: { flushSync?: boolean } = {}
  1072. ): void {
  1073. // Deduce if we're in a loading/actionReload state:
  1074. // - We have committed actionData in the store
  1075. // - The current navigation was a mutation submission
  1076. // - We're past the submitting state and into the loading state
  1077. // - The location being loaded is not the result of a redirect
  1078. let isActionReload =
  1079. state.actionData != null &&
  1080. state.navigation.formMethod != null &&
  1081. isMutationMethod(state.navigation.formMethod) &&
  1082. state.navigation.state === "loading" &&
  1083. location.state?._isRedirect !== true;
  1084. let actionData: RouteData | null;
  1085. if (newState.actionData) {
  1086. if (Object.keys(newState.actionData).length > 0) {
  1087. actionData = newState.actionData;
  1088. } else {
  1089. // Empty actionData -> clear prior actionData due to an action error
  1090. actionData = null;
  1091. }
  1092. } else if (isActionReload) {
  1093. // Keep the current data if we're wrapping up the action reload
  1094. actionData = state.actionData;
  1095. } else {
  1096. // Clear actionData on any other completed navigations
  1097. actionData = null;
  1098. }
  1099. // Always preserve any existing loaderData from re-used routes
  1100. let loaderData = newState.loaderData
  1101. ? mergeLoaderData(
  1102. state.loaderData,
  1103. newState.loaderData,
  1104. newState.matches || [],
  1105. newState.errors
  1106. )
  1107. : state.loaderData;
  1108. // On a successful navigation we can assume we got through all blockers
  1109. // so we can start fresh
  1110. let blockers = state.blockers;
  1111. if (blockers.size > 0) {
  1112. blockers = new Map(blockers);
  1113. blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER));
  1114. }
  1115. // Always respect the user flag. Otherwise don't reset on mutation
  1116. // submission navigations unless they redirect
  1117. let preventScrollReset =
  1118. pendingPreventScrollReset === true ||
  1119. (state.navigation.formMethod != null &&
  1120. isMutationMethod(state.navigation.formMethod) &&
  1121. location.state?._isRedirect !== true);
  1122. // Commit any in-flight routes at the end of the HMR revalidation "navigation"
  1123. if (inFlightDataRoutes) {
  1124. dataRoutes = inFlightDataRoutes;
  1125. inFlightDataRoutes = undefined;
  1126. }
  1127. if (isUninterruptedRevalidation) {
  1128. // If this was an uninterrupted revalidation then do not touch history
  1129. } else if (pendingAction === HistoryAction.Pop) {
  1130. // Do nothing for POP - URL has already been updated
  1131. } else if (pendingAction === HistoryAction.Push) {
  1132. init.history.push(location, location.state);
  1133. } else if (pendingAction === HistoryAction.Replace) {
  1134. init.history.replace(location, location.state);
  1135. }
  1136. let viewTransitionOpts: ViewTransitionOpts | undefined;
  1137. // On POP, enable transitions if they were enabled on the original navigation
  1138. if (pendingAction === HistoryAction.Pop) {
  1139. // Forward takes precedence so they behave like the original navigation
  1140. let priorPaths = appliedViewTransitions.get(state.location.pathname);
  1141. if (priorPaths && priorPaths.has(location.pathname)) {
  1142. viewTransitionOpts = {
  1143. currentLocation: state.location,
  1144. nextLocation: location,
  1145. };
  1146. } else if (appliedViewTransitions.has(location.pathname)) {
  1147. // If we don't have a previous forward nav, assume we're popping back to
  1148. // the new location and enable if that location previously enabled
  1149. viewTransitionOpts = {
  1150. currentLocation: location,
  1151. nextLocation: state.location,
  1152. };
  1153. }
  1154. } else if (pendingViewTransitionEnabled) {
  1155. // Store the applied transition on PUSH/REPLACE
  1156. let toPaths = appliedViewTransitions.get(state.location.pathname);
  1157. if (toPaths) {
  1158. toPaths.add(location.pathname);
  1159. } else {
  1160. toPaths = new Set<string>([location.pathname]);
  1161. appliedViewTransitions.set(state.location.pathname, toPaths);
  1162. }
  1163. viewTransitionOpts = {
  1164. currentLocation: state.location,
  1165. nextLocation: location,
  1166. };
  1167. }
  1168. updateState(
  1169. {
  1170. ...newState, // matches, errors, fetchers go through as-is
  1171. actionData,
  1172. loaderData,
  1173. historyAction: pendingAction,
  1174. location,
  1175. initialized: true,
  1176. navigation: IDLE_NAVIGATION,
  1177. revalidation: "idle",
  1178. restoreScrollPosition: getSavedScrollPosition(
  1179. location,
  1180. newState.matches || state.matches
  1181. ),
  1182. preventScrollReset,
  1183. blockers,
  1184. },
  1185. {
  1186. viewTransitionOpts,
  1187. flushSync: flushSync === true,
  1188. }
  1189. );
  1190. // Reset stateful navigation vars
  1191. pendingAction = HistoryAction.Pop;
  1192. pendingPreventScrollReset = false;
  1193. pendingViewTransitionEnabled = false;
  1194. isUninterruptedRevalidation = false;
  1195. isRevalidationRequired = false;
  1196. cancelledDeferredRoutes = [];
  1197. }
  1198. // Trigger a navigation event, which can either be a numerical POP or a PUSH
  1199. // replace with an optional submission
  1200. async function navigate(
  1201. to: number | To | null,
  1202. opts?: RouterNavigateOptions
  1203. ): Promise<void> {
  1204. if (typeof to === "number") {
  1205. init.history.go(to);
  1206. return;
  1207. }
  1208. let normalizedPath = normalizeTo(
  1209. state.location,
  1210. state.matches,
  1211. basename,
  1212. future.v7_prependBasename,
  1213. to,
  1214. future.v7_relativeSplatPath,
  1215. opts?.fromRouteId,
  1216. opts?.relative
  1217. );
  1218. let { path, submission, error } = normalizeNavigateOptions(
  1219. future.v7_normalizeFormMethod,
  1220. false,
  1221. normalizedPath,
  1222. opts
  1223. );
  1224. let currentLocation = state.location;
  1225. let nextLocation = createLocation(state.location, path, opts && opts.state);
  1226. // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
  1227. // URL from window.location, so we need to encode it here so the behavior
  1228. // remains the same as POP and non-data-router usages. new URL() does all
  1229. // the same encoding we'd get from a history.pushState/window.location read
  1230. // without having to touch history
  1231. nextLocation = {
  1232. ...nextLocation,
  1233. ...init.history.encodeLocation(nextLocation),
  1234. };
  1235. let userReplace = opts && opts.replace != null ? opts.replace : undefined;
  1236. let historyAction = HistoryAction.Push;
  1237. if (userReplace === true) {
  1238. historyAction = HistoryAction.Replace;
  1239. } else if (userReplace === false) {
  1240. // no-op
  1241. } else if (
  1242. submission != null &&
  1243. isMutationMethod(submission.formMethod) &&
  1244. submission.formAction === state.location.pathname + state.location.search
  1245. ) {
  1246. // By default on submissions to the current location we REPLACE so that
  1247. // users don't have to double-click the back button to get to the prior
  1248. // location. If the user redirects to a different location from the
  1249. // action/loader this will be ignored and the redirect will be a PUSH
  1250. historyAction = HistoryAction.Replace;
  1251. }
  1252. let preventScrollReset =
  1253. opts && "preventScrollReset" in opts
  1254. ? opts.preventScrollReset === true
  1255. : undefined;
  1256. let flushSync = (opts && opts.flushSync) === true;
  1257. let blockerKey = shouldBlockNavigation({
  1258. currentLocation,
  1259. nextLocation,
  1260. historyAction,
  1261. });
  1262. if (blockerKey) {
  1263. // Put the blocker into a blocked state
  1264. updateBlocker(blockerKey, {
  1265. state: "blocked",
  1266. location: nextLocation,
  1267. proceed() {
  1268. updateBlocker(blockerKey!, {
  1269. state: "proceeding",
  1270. proceed: undefined,
  1271. reset: undefined,
  1272. location: nextLocation,
  1273. });
  1274. // Send the same navigation through
  1275. navigate(to, opts);
  1276. },
  1277. reset() {
  1278. let blockers = new Map(state.blockers);
  1279. blockers.set(blockerKey!, IDLE_BLOCKER);
  1280. updateState({ blockers });
  1281. },
  1282. });
  1283. return;
  1284. }
  1285. return await startNavigation(historyAction, nextLocation, {
  1286. submission,
  1287. // Send through the formData serialization error if we have one so we can
  1288. // render at the right error boundary after we match routes
  1289. pendingError: error,
  1290. preventScrollReset,
  1291. replace: opts && opts.replace,
  1292. enableViewTransition: opts && opts.viewTransition,
  1293. flushSync,
  1294. });
  1295. }
  1296. // Revalidate all current loaders. If a navigation is in progress or if this
  1297. // is interrupted by a navigation, allow this to "succeed" by calling all
  1298. // loaders during the next loader round
  1299. function revalidate() {
  1300. interruptActiveLoads();
  1301. updateState({ revalidation: "loading" });
  1302. // If we're currently submitting an action, we don't need to start a new
  1303. // navigation, we'll just let the follow up loader execution call all loaders
  1304. if (state.navigation.state === "submitting") {
  1305. return;
  1306. }
  1307. // If we're currently in an idle state, start a new navigation for the current
  1308. // action/location and mark it as uninterrupted, which will skip the history
  1309. // update in completeNavigation
  1310. if (state.navigation.state === "idle") {
  1311. startNavigation(state.historyAction, state.location, {
  1312. startUninterruptedRevalidation: true,
  1313. });
  1314. return;
  1315. }
  1316. // Otherwise, if we're currently in a loading state, just start a new
  1317. // navigation to the navigation.location but do not trigger an uninterrupted
  1318. // revalidation so that history correctly updates once the navigation completes
  1319. startNavigation(
  1320. pendingAction || state.historyAction,
  1321. state.navigation.location,
  1322. {
  1323. overrideNavigation: state.navigation,
  1324. // Proxy through any rending view transition
  1325. enableViewTransition: pendingViewTransitionEnabled === true,
  1326. }
  1327. );
  1328. }
  1329. // Start a navigation to the given action/location. Can optionally provide a
  1330. // overrideNavigation which will override the normalLoad in the case of a redirect
  1331. // navigation
  1332. async function startNavigation(
  1333. historyAction: HistoryAction,
  1334. location: Location,
  1335. opts?: {
  1336. initialHydration?: boolean;
  1337. submission?: Submission;
  1338. fetcherSubmission?: Submission;
  1339. overrideNavigation?: Navigation;
  1340. pendingError?: ErrorResponseImpl;
  1341. startUninterruptedRevalidation?: boolean;
  1342. preventScrollReset?: boolean;
  1343. replace?: boolean;
  1344. enableViewTransition?: boolean;
  1345. flushSync?: boolean;
  1346. }
  1347. ): Promise<void> {
  1348. // Abort any in-progress navigations and start a new one. Unset any ongoing
  1349. // uninterrupted revalidations unless told otherwise, since we want this
  1350. // new navigation to update history normally
  1351. pendingNavigationController && pendingNavigationController.abort();
  1352. pendingNavigationController = null;
  1353. pendingAction = historyAction;
  1354. isUninterruptedRevalidation =
  1355. (opts && opts.startUninterruptedRevalidation) === true;
  1356. // Save the current scroll position every time we start a new navigation,
  1357. // and track whether we should reset scroll on completion
  1358. saveScrollPosition(state.location, state.matches);
  1359. pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
  1360. pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
  1361. let routesToUse = inFlightDataRoutes || dataRoutes;
  1362. let loadingNavigation = opts && opts.overrideNavigation;
  1363. let matches =
  1364. opts?.initialHydration &&
  1365. state.matches &&
  1366. state.matches.length > 0 &&
  1367. !initialMatchesIsFOW
  1368. ? // `matchRoutes()` has already been called if we're in here via `router.initialize()`
  1369. state.matches
  1370. : matchRoutes(routesToUse, location, basename);
  1371. let flushSync = (opts && opts.flushSync) === true;
  1372. // Short circuit if it's only a hash change and not a revalidation or
  1373. // mutation submission.
  1374. //
  1375. // Ignore on initial page loads because since the initial hydration will always
  1376. // be "same hash". For example, on /page#hash and submit a <Form method="post">
  1377. // which will default to a navigation to /page
  1378. if (
  1379. matches &&
  1380. state.initialized &&
  1381. !isRevalidationRequired &&
  1382. isHashChangeOnly(state.location, location) &&
  1383. !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))
  1384. ) {
  1385. completeNavigation(location, { matches }, { flushSync });
  1386. return;
  1387. }
  1388. let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);
  1389. if (fogOfWar.active && fogOfWar.matches) {
  1390. matches = fogOfWar.matches;
  1391. }
  1392. // Short circuit with a 404 on the root error boundary if we match nothing
  1393. if (!matches) {
  1394. let { error, notFoundMatches, route } = handleNavigational404(
  1395. location.pathname
  1396. );
  1397. completeNavigation(
  1398. location,
  1399. {
  1400. matches: notFoundMatches,
  1401. loaderData: {},
  1402. errors: {
  1403. [route.id]: error,
  1404. },
  1405. },
  1406. { flushSync }
  1407. );
  1408. return;
  1409. }
  1410. // Create a controller/Request for this navigation
  1411. pendingNavigationController = new AbortController();
  1412. let request = createClientSideRequest(
  1413. init.history,
  1414. location,
  1415. pendingNavigationController.signal,
  1416. opts && opts.submission
  1417. );
  1418. let pendingActionResult: PendingActionResult | undefined;
  1419. if (opts && opts.pendingError) {
  1420. // If we have a pendingError, it means the user attempted a GET submission
  1421. // with binary FormData so assign here and skip to handleLoaders. That
  1422. // way we handle calling loaders above the boundary etc. It's not really
  1423. // different from an actionError in that sense.
  1424. pendingActionResult = [
  1425. findNearestBoundary(matches).route.id,
  1426. { type: ResultType.error, error: opts.pendingError },
  1427. ];
  1428. } else if (
  1429. opts &&
  1430. opts.submission &&
  1431. isMutationMethod(opts.submission.formMethod)
  1432. ) {
  1433. // Call action if we received an action submission
  1434. let actionResult = await handleAction(
  1435. request,
  1436. location,
  1437. opts.submission,
  1438. matches,
  1439. fogOfWar.active,
  1440. { replace: opts.replace, flushSync }
  1441. );
  1442. if (actionResult.shortCircuited) {
  1443. return;
  1444. }
  1445. // If we received a 404 from handleAction, it's because we couldn't lazily
  1446. // discover the destination route so we don't want to call loaders
  1447. if (actionResult.pendingActionResult) {
  1448. let [routeId, result] = actionResult.pendingActionResult;
  1449. if (
  1450. isErrorResult(result) &&
  1451. isRouteErrorResponse(result.error) &&
  1452. result.error.status === 404
  1453. ) {
  1454. pendingNavigationController = null;
  1455. completeNavigation(location, {
  1456. matches: actionResult.matches,
  1457. loaderData: {},
  1458. errors: {
  1459. [routeId]: result.error,
  1460. },
  1461. });
  1462. return;
  1463. }
  1464. }
  1465. matches = actionResult.matches || matches;
  1466. pendingActionResult = actionResult.pendingActionResult;
  1467. loadingNavigation = getLoadingNavigation(location, opts.submission);
  1468. flushSync = false;
  1469. // No need to do fog of war matching again on loader execution
  1470. fogOfWar.active = false;
  1471. // Create a GET request for the loaders
  1472. request = createClientSideRequest(
  1473. init.history,
  1474. request.url,
  1475. request.signal
  1476. );
  1477. }
  1478. // Call loaders
  1479. let {
  1480. shortCircuited,
  1481. matches: updatedMatches,
  1482. loaderData,
  1483. errors,
  1484. } = await handleLoaders(
  1485. request,
  1486. location,
  1487. matches,
  1488. fogOfWar.active,
  1489. loadingNavigation,
  1490. opts && opts.submission,
  1491. opts && opts.fetcherSubmission,
  1492. opts && opts.replace,
  1493. opts && opts.initialHydration === true,
  1494. flushSync,
  1495. pendingActionResult
  1496. );
  1497. if (shortCircuited) {
  1498. return;
  1499. }
  1500. // Clean up now that the action/loaders have completed. Don't clean up if
  1501. // we short circuited because pendingNavigationController will have already
  1502. // been assigned to a new controller for the next navigation
  1503. pendingNavigationController = null;
  1504. completeNavigation(location, {
  1505. matches: updatedMatches || matches,
  1506. ...getActionDataForCommit(pendingActionResult),
  1507. loaderData,
  1508. errors,
  1509. });
  1510. }
  1511. // Call the action matched by the leaf route for this navigation and handle
  1512. // redirects/errors
  1513. async function handleAction(
  1514. request: Request,
  1515. location: Location,
  1516. submission: Submission,
  1517. matches: AgnosticDataRouteMatch[],
  1518. isFogOfWar: boolean,
  1519. opts: { replace?: boolean; flushSync?: boolean } = {}
  1520. ): Promise<HandleActionResult> {
  1521. interruptActiveLoads();
  1522. // Put us in a submitting state
  1523. let navigation = getSubmittingNavigation(location, submission);
  1524. updateState({ navigation }, { flushSync: opts.flushSync === true });
  1525. if (isFogOfWar) {
  1526. let discoverResult = await discoverRoutes(
  1527. matches,
  1528. location.pathname,
  1529. request.signal
  1530. );
  1531. if (discoverResult.type === "aborted") {
  1532. return { shortCircuited: true };
  1533. } else if (discoverResult.type === "error") {
  1534. let boundaryId = findNearestBoundary(discoverResult.partialMatches)
  1535. .route.id;
  1536. return {
  1537. matches: discoverResult.partialMatches,
  1538. pendingActionResult: [
  1539. boundaryId,
  1540. {
  1541. type: ResultType.error,
  1542. error: discoverResult.error,
  1543. },
  1544. ],
  1545. };
  1546. } else if (!discoverResult.matches) {
  1547. let { notFoundMatches, error, route } = handleNavigational404(
  1548. location.pathname
  1549. );
  1550. return {
  1551. matches: notFoundMatches,
  1552. pendingActionResult: [
  1553. route.id,
  1554. {
  1555. type: ResultType.error,
  1556. error,
  1557. },
  1558. ],
  1559. };
  1560. } else {
  1561. matches = discoverResult.matches;
  1562. }
  1563. }
  1564. // Call our action and get the result
  1565. let result: DataResult;
  1566. let actionMatch = getTargetMatch(matches, location);
  1567. if (!actionMatch.route.action && !actionMatch.route.lazy) {
  1568. result = {
  1569. type: ResultType.error,
  1570. error: getInternalRouterError(405, {
  1571. method: request.method,
  1572. pathname: location.pathname,
  1573. routeId: actionMatch.route.id,
  1574. }),
  1575. };
  1576. } else {
  1577. let results = await callDataStrategy(
  1578. "action",
  1579. state,
  1580. request,
  1581. [actionMatch],
  1582. matches,
  1583. null
  1584. );
  1585. result = results[actionMatch.route.id];
  1586. if (request.signal.aborted) {
  1587. return { shortCircuited: true };
  1588. }
  1589. }
  1590. if (isRedirectResult(result)) {
  1591. let replace: boolean;
  1592. if (opts && opts.replace != null) {
  1593. replace = opts.replace;
  1594. } else {
  1595. // If the user didn't explicity indicate replace behavior, replace if
  1596. // we redirected to the exact same location we're currently at to avoid
  1597. // double back-buttons
  1598. let location = normalizeRedirectLocation(
  1599. result.response.headers.get("Location")!,
  1600. new URL(request.url),
  1601. basename,
  1602. init.history,
  1603. );
  1604. replace = location === state.location.pathname + state.location.search;
  1605. }
  1606. await startRedirectNavigation(request, result, true, {
  1607. submission,
  1608. replace,
  1609. });
  1610. return { shortCircuited: true };
  1611. }
  1612. if (isDeferredResult(result)) {
  1613. throw getInternalRouterError(400, { type: "defer-action" });
  1614. }
  1615. if (isErrorResult(result)) {
  1616. // Store off the pending error - we use it to determine which loaders
  1617. // to call and will commit it when we complete the navigation
  1618. let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
  1619. // By default, all submissions to the current location are REPLACE
  1620. // navigations, but if the action threw an error that'll be rendered in
  1621. // an errorElement, we fall back to PUSH so that the user can use the
  1622. // back button to get back to the pre-submission form location to try
  1623. // again
  1624. if ((opts && opts.replace) !== true) {
  1625. pendingAction = HistoryAction.Push;
  1626. }
  1627. return {
  1628. matches,
  1629. pendingActionResult: [boundaryMatch.route.id, result],
  1630. };
  1631. }
  1632. return {
  1633. matches,
  1634. pendingActionResult: [actionMatch.route.id, result],
  1635. };
  1636. }
  1637. // Call all applicable loaders for the given matches, handling redirects,
  1638. // errors, etc.
  1639. async function handleLoaders(
  1640. request: Request,
  1641. location: Location,
  1642. matches: AgnosticDataRouteMatch[],
  1643. isFogOfWar: boolean,
  1644. overrideNavigation?: Navigation,
  1645. submission?: Submission,
  1646. fetcherSubmission?: Submission,
  1647. replace?: boolean,
  1648. initialHydration?: boolean,
  1649. flushSync?: boolean,
  1650. pendingActionResult?: PendingActionResult
  1651. ): Promise<HandleLoadersResult> {
  1652. // Figure out the right navigation we want to use for data loading
  1653. let loadingNavigation =
  1654. overrideNavigation || getLoadingNavigation(location, submission);
  1655. // If this was a redirect from an action we don't have a "submission" but
  1656. // we have it on the loading navigation so use that if available
  1657. let activeSubmission =
  1658. submission ||
  1659. fetcherSubmission ||
  1660. getSubmissionFromNavigation(loadingNavigation);
  1661. // If this is an uninterrupted revalidation, we remain in our current idle
  1662. // state. If not, we need to switch to our loading state and load data,
  1663. // preserving any new action data or existing action data (in the case of
  1664. // a revalidation interrupting an actionReload)
  1665. // If we have partialHydration enabled, then don't update the state for the
  1666. // initial data load since it's not a "navigation"
  1667. let shouldUpdateNavigationState =
  1668. !isUninterruptedRevalidation &&
  1669. (!future.v7_partialHydration || !initialHydration);
  1670. // When fog of war is enabled, we enter our `loading` state earlier so we
  1671. // can discover new routes during the `loading` state. We skip this if
  1672. // we've already run actions since we would have done our matching already.
  1673. // If the children() function threw then, we want to proceed with the
  1674. // partial matches it discovered.
  1675. if (isFogOfWar) {
  1676. if (shouldUpdateNavigationState) {
  1677. let actionData = getUpdatedActionData(pendingActionResult);
  1678. updateState(
  1679. {
  1680. navigation: loadingNavigation,
  1681. ...(actionData !== undefined ? { actionData } : {}),
  1682. },
  1683. {
  1684. flushSync,
  1685. }
  1686. );
  1687. }
  1688. let discoverResult = await discoverRoutes(
  1689. matches,
  1690. location.pathname,
  1691. request.signal
  1692. );
  1693. if (discoverResult.type === "aborted") {
  1694. return { shortCircuited: true };
  1695. } else if (discoverResult.type === "error") {
  1696. let boundaryId = findNearestBoundary(discoverResult.partialMatches)
  1697. .route.id;
  1698. return {
  1699. matches: discoverResult.partialMatches,
  1700. loaderData: {},
  1701. errors: {
  1702. [boundaryId]: discoverResult.error,
  1703. },
  1704. };
  1705. } else if (!discoverResult.matches) {
  1706. let { error, notFoundMatches, route } = handleNavigational404(
  1707. location.pathname
  1708. );
  1709. return {
  1710. matches: notFoundMatches,
  1711. loaderData: {},
  1712. errors: {
  1713. [route.id]: error,
  1714. },
  1715. };
  1716. } else {
  1717. matches = discoverResult.matches;
  1718. }
  1719. }
  1720. let routesToUse = inFlightDataRoutes || dataRoutes;
  1721. let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
  1722. init.history,
  1723. state,
  1724. matches,
  1725. activeSubmission,
  1726. location,
  1727. future.v7_partialHydration && initialHydration === true,
  1728. future.v7_skipActionErrorRevalidation,
  1729. isRevalidationRequired,
  1730. cancelledDeferredRoutes,
  1731. cancelledFetcherLoads,
  1732. deletedFetchers,
  1733. fetchLoadMatches,
  1734. fetchRedirectIds,
  1735. routesToUse,
  1736. basename,
  1737. pendingActionResult
  1738. );
  1739. // Cancel pending deferreds for no-longer-matched routes or routes we're
  1740. // about to reload. Note that if this is an action reload we would have
  1741. // already cancelled all pending deferreds so this would be a no-op
  1742. cancelActiveDeferreds(
  1743. (routeId) =>
  1744. !(matches && matches.some((m) => m.route.id === routeId)) ||
  1745. (matchesToLoad && matchesToLoad.some((m) => m.route.id === routeId))
  1746. );
  1747. pendingNavigationLoadId = ++incrementingLoadId;
  1748. // Short circuit if we have no loaders to run
  1749. if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
  1750. let updatedFetchers = markFetchRedirectsDone();
  1751. completeNavigation(
  1752. location,
  1753. {
  1754. matches,
  1755. loaderData: {},
  1756. // Commit pending error if we're short circuiting
  1757. errors:
  1758. pendingActionResult && isErrorResult(pendingActionResult[1])
  1759. ? { [pendingActionResult[0]]: pendingActionResult[1].error }
  1760. : null,
  1761. ...getActionDataForCommit(pendingActionResult),
  1762. ...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
  1763. },
  1764. { flushSync }
  1765. );
  1766. return { shortCircuited: true };
  1767. }
  1768. if (shouldUpdateNavigationState) {
  1769. let updates: Partial<RouterState> = {};
  1770. if (!isFogOfWar) {
  1771. // Only update navigation/actionNData if we didn't already do it above
  1772. updates.navigation = loadingNavigation;
  1773. let actionData = getUpdatedActionData(pendingActionResult);
  1774. if (actionData !== undefined) {
  1775. updates.actionData = actionData;
  1776. }
  1777. }
  1778. if (revalidatingFetchers.length > 0) {
  1779. updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers);
  1780. }
  1781. updateState(updates, { flushSync });
  1782. }
  1783. revalidatingFetchers.forEach((rf) => {
  1784. abortFetcher(rf.key);
  1785. if (rf.controller) {
  1786. // Fetchers use an independent AbortController so that aborting a fetcher
  1787. // (via deleteFetcher) does not abort the triggering navigation that
  1788. // triggered the revalidation
  1789. fetchControllers.set(rf.key, rf.controller);
  1790. }
  1791. });
  1792. // Proxy navigation abort through to revalidation fetchers
  1793. let abortPendingFetchRevalidations = () =>
  1794. revalidatingFetchers.forEach((f) => abortFetcher(f.key));
  1795. if (pendingNavigationController) {
  1796. pendingNavigationController.signal.addEventListener(
  1797. "abort",
  1798. abortPendingFetchRevalidations
  1799. );
  1800. }
  1801. let { loaderResults, fetcherResults } =
  1802. await callLoadersAndMaybeResolveData(
  1803. state,
  1804. matches,
  1805. matchesToLoad,
  1806. revalidatingFetchers,
  1807. request
  1808. );
  1809. if (request.signal.aborted) {
  1810. return { shortCircuited: true };
  1811. }
  1812. // Clean up _after_ loaders have completed. Don't clean up if we short
  1813. // circuited because fetchControllers would have been aborted and
  1814. // reassigned to new controllers for the next navigation
  1815. if (pendingNavigationController) {
  1816. pendingNavigationController.signal.removeEventListener(
  1817. "abort",
  1818. abortPendingFetchRevalidations
  1819. );
  1820. }
  1821. revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
  1822. // If any loaders returned a redirect Response, start a new REPLACE navigation
  1823. let redirect = findRedirect(loaderResults);
  1824. if (redirect) {
  1825. await startRedirectNavigation(request, redirect.result, true, {
  1826. replace,
  1827. });
  1828. return { shortCircuited: true };
  1829. }
  1830. redirect = findRedirect(fetcherResults);
  1831. if (redirect) {
  1832. // If this redirect came from a fetcher make sure we mark it in
  1833. // fetchRedirectIds so it doesn't get revalidated on the next set of
  1834. // loader executions
  1835. fetchRedirectIds.add(redirect.key);
  1836. await startRedirectNavigation(request, redirect.result, true, {
  1837. replace,
  1838. });
  1839. return { shortCircuited: true };
  1840. }
  1841. // Process and commit output from loaders
  1842. let { loaderData, errors } = processLoaderData(
  1843. state,
  1844. matches,
  1845. loaderResults,
  1846. pendingActionResult,
  1847. revalidatingFetchers,
  1848. fetcherResults,
  1849. activeDeferreds
  1850. );
  1851. // Wire up subscribers to update loaderData as promises settle
  1852. activeDeferreds.forEach((deferredData, routeId) => {
  1853. deferredData.subscribe((aborted) => {
  1854. // Note: No need to updateState here since the TrackedPromise on
  1855. // loaderData is stable across resolve/reject
  1856. // Remove this instance if we were aborted or if promises have settled
  1857. if (aborted || deferredData.done) {
  1858. activeDeferreds.delete(routeId);
  1859. }
  1860. });
  1861. });
  1862. // Preserve SSR errors during partial hydration
  1863. if (future.v7_partialHydration && initialHydration && state.errors) {
  1864. errors = { ...state.errors, ...errors };
  1865. }
  1866. let updatedFetchers = markFetchRedirectsDone();
  1867. let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
  1868. let shouldUpdateFetchers =
  1869. updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
  1870. return {
  1871. matches,
  1872. loaderData,
  1873. errors,
  1874. ...(shouldUpdateFetchers ? { fetchers: new Map(state.fetchers) } : {}),
  1875. };
  1876. }
  1877. function getUpdatedActionData(
  1878. pendingActionResult: PendingActionResult | undefined
  1879. ): Record<string, RouteData> | null | undefined {
  1880. if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
  1881. // This is cast to `any` currently because `RouteData`uses any and it
  1882. // would be a breaking change to use any.
  1883. // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
  1884. return {
  1885. [pendingActionResult[0]]: pendingActionResult[1].data as any,
  1886. };
  1887. } else if (state.actionData) {
  1888. if (Object.keys(state.actionData).length === 0) {
  1889. return null;
  1890. } else {
  1891. return state.actionData;
  1892. }
  1893. }
  1894. }
  1895. function getUpdatedRevalidatingFetchers(
  1896. revalidatingFetchers: RevalidatingFetcher[]
  1897. ) {
  1898. revalidatingFetchers.forEach((rf) => {
  1899. let fetcher = state.fetchers.get(rf.key);
  1900. let revalidatingFetcher = getLoadingFetcher(
  1901. undefined,
  1902. fetcher ? fetcher.data : undefined
  1903. );
  1904. state.fetchers.set(rf.key, revalidatingFetcher);
  1905. });
  1906. return new Map(state.fetchers);
  1907. }
  1908. // Trigger a fetcher load/submit for the given fetcher key
  1909. function fetch(
  1910. key: string,
  1911. routeId: string,
  1912. href: string | null,
  1913. opts?: RouterFetchOptions
  1914. ) {
  1915. if (isServer) {
  1916. throw new Error(
  1917. "router.fetch() was called during the server render, but it shouldn't be. " +
  1918. "You are likely calling a useFetcher() method in the body of your component. " +
  1919. "Try moving it to a useEffect or a callback."
  1920. );
  1921. }
  1922. abortFetcher(key);
  1923. let flushSync = (opts && opts.flushSync) === true;
  1924. let routesToUse = inFlightDataRoutes || dataRoutes;
  1925. let normalizedPath = normalizeTo(
  1926. state.location,
  1927. state.matches,
  1928. basename,
  1929. future.v7_prependBasename,
  1930. href,
  1931. future.v7_relativeSplatPath,
  1932. routeId,
  1933. opts?.relative
  1934. );
  1935. let matches = matchRoutes(routesToUse, normalizedPath, basename);
  1936. let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath);
  1937. if (fogOfWar.active && fogOfWar.matches) {
  1938. matches = fogOfWar.matches;
  1939. }
  1940. if (!matches) {
  1941. setFetcherError(
  1942. key,
  1943. routeId,
  1944. getInternalRouterError(404, { pathname: normalizedPath }),
  1945. { flushSync }
  1946. );
  1947. return;
  1948. }
  1949. let { path, submission, error } = normalizeNavigateOptions(
  1950. future.v7_normalizeFormMethod,
  1951. true,
  1952. normalizedPath,
  1953. opts
  1954. );
  1955. if (error) {
  1956. setFetcherError(key, routeId, error, { flushSync });
  1957. return;
  1958. }
  1959. let match = getTargetMatch(matches, path);
  1960. let preventScrollReset = (opts && opts.preventScrollReset) === true;
  1961. if (submission && isMutationMethod(submission.formMethod)) {
  1962. handleFetcherAction(
  1963. key,
  1964. routeId,
  1965. path,
  1966. match,
  1967. matches,
  1968. fogOfWar.active,
  1969. flushSync,
  1970. preventScrollReset,
  1971. submission
  1972. );
  1973. return;
  1974. }
  1975. // Store off the match so we can call it's shouldRevalidate on subsequent
  1976. // revalidations
  1977. fetchLoadMatches.set(key, { routeId, path });
  1978. handleFetcherLoader(
  1979. key,
  1980. routeId,
  1981. path,
  1982. match,
  1983. matches,
  1984. fogOfWar.active,
  1985. flushSync,
  1986. preventScrollReset,
  1987. submission
  1988. );
  1989. }
  1990. // Call the action for the matched fetcher.submit(), and then handle redirects,
  1991. // errors, and revalidation
  1992. async function handleFetcherAction(
  1993. key: string,
  1994. routeId: string,
  1995. path: string,
  1996. match: AgnosticDataRouteMatch,
  1997. requestMatches: AgnosticDataRouteMatch[],
  1998. isFogOfWar: boolean,
  1999. flushSync: boolean,
  2000. preventScrollReset: boolean,
  2001. submission: Submission
  2002. ) {
  2003. interruptActiveLoads();
  2004. fetchLoadMatches.delete(key);
  2005. function detectAndHandle405Error(m: AgnosticDataRouteMatch) {
  2006. if (!m.route.action && !m.route.lazy) {
  2007. let error = getInternalRouterError(405, {
  2008. method: submission.formMethod,
  2009. pathname: path,
  2010. routeId: routeId,
  2011. });
  2012. setFetcherError(key, routeId, error, { flushSync });
  2013. return true;
  2014. }
  2015. return false;
  2016. }
  2017. if (!isFogOfWar && detectAndHandle405Error(match)) {
  2018. return;
  2019. }
  2020. // Put this fetcher into it's submitting state
  2021. let existingFetcher = state.fetchers.get(key);
  2022. updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
  2023. flushSync,
  2024. });
  2025. let abortController = new AbortController();
  2026. let fetchRequest = createClientSideRequest(
  2027. init.history,
  2028. path,
  2029. abortController.signal,
  2030. submission
  2031. );
  2032. if (isFogOfWar) {
  2033. let discoverResult = await discoverRoutes(
  2034. requestMatches,
  2035. new URL(fetchRequest.url).pathname,
  2036. fetchRequest.signal,
  2037. key
  2038. );
  2039. if (discoverResult.type === "aborted") {
  2040. return;
  2041. } else if (discoverResult.type === "error") {
  2042. setFetcherError(key, routeId, discoverResult.error, { flushSync });
  2043. return;
  2044. } else if (!discoverResult.matches) {
  2045. setFetcherError(
  2046. key,
  2047. routeId,
  2048. getInternalRouterError(404, { pathname: path }),
  2049. { flushSync }
  2050. );
  2051. return;
  2052. } else {
  2053. requestMatches = discoverResult.matches;
  2054. match = getTargetMatch(requestMatches, path);
  2055. if (detectAndHandle405Error(match)) {
  2056. return;
  2057. }
  2058. }
  2059. }
  2060. // Call the action for the fetcher
  2061. fetchControllers.set(key, abortController);
  2062. let originatingLoadId = incrementingLoadId;
  2063. let actionResults = await callDataStrategy(
  2064. "action",
  2065. state,
  2066. fetchRequest,
  2067. [match],
  2068. requestMatches,
  2069. key
  2070. );
  2071. let actionResult = actionResults[match.route.id];
  2072. if (fetchRequest.signal.aborted) {
  2073. // We can delete this so long as we weren't aborted by our own fetcher
  2074. // re-submit which would have put _new_ controller is in fetchControllers
  2075. if (fetchControllers.get(key) === abortController) {
  2076. fetchControllers.delete(key);
  2077. }
  2078. return;
  2079. }
  2080. // When using v7_fetcherPersist, we don't want errors bubbling up to the UI
  2081. // or redirects processed for unmounted fetchers so we just revert them to
  2082. // idle
  2083. if (future.v7_fetcherPersist && deletedFetchers.has(key)) {
  2084. if (isRedirectResult(actionResult) || isErrorResult(actionResult)) {
  2085. updateFetcherState(key, getDoneFetcher(undefined));
  2086. return;
  2087. }
  2088. // Let SuccessResult's fall through for revalidation
  2089. } else {
  2090. if (isRedirectResult(actionResult)) {
  2091. fetchControllers.delete(key);
  2092. if (pendingNavigationLoadId > originatingLoadId) {
  2093. // A new navigation was kicked off after our action started, so that
  2094. // should take precedence over this redirect navigation. We already
  2095. // set isRevalidationRequired so all loaders for the new route should
  2096. // fire unless opted out via shouldRevalidate
  2097. updateFetcherState(key, getDoneFetcher(undefined));
  2098. return;
  2099. } else {
  2100. fetchRedirectIds.add(key);
  2101. updateFetcherState(key, getLoadingFetcher(submission));
  2102. return startRedirectNavigation(fetchRequest, actionResult, false, {
  2103. fetcherSubmission: submission,
  2104. preventScrollReset,
  2105. });
  2106. }
  2107. }
  2108. // Process any non-redirect errors thrown
  2109. if (isErrorResult(actionResult)) {
  2110. setFetcherError(key, routeId, actionResult.error);
  2111. return;
  2112. }
  2113. }
  2114. if (isDeferredResult(actionResult)) {
  2115. throw getInternalRouterError(400, { type: "defer-action" });
  2116. }
  2117. // Start the data load for current matches, or the next location if we're
  2118. // in the middle of a navigation
  2119. let nextLocation = state.navigation.location || state.location;
  2120. let revalidationRequest = createClientSideRequest(
  2121. init.history,
  2122. nextLocation,
  2123. abortController.signal
  2124. );
  2125. let routesToUse = inFlightDataRoutes || dataRoutes;
  2126. let matches =
  2127. state.navigation.state !== "idle"
  2128. ? matchRoutes(routesToUse, state.navigation.location, basename)
  2129. : state.matches;
  2130. invariant(matches, "Didn't find any matches after fetcher action");
  2131. let loadId = ++incrementingLoadId;
  2132. fetchReloadIds.set(key, loadId);
  2133. let loadFetcher = getLoadingFetcher(submission, actionResult.data);
  2134. state.fetchers.set(key, loadFetcher);
  2135. let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
  2136. init.history,
  2137. state,
  2138. matches,
  2139. submission,
  2140. nextLocation,
  2141. false,
  2142. future.v7_skipActionErrorRevalidation,
  2143. isRevalidationRequired,
  2144. cancelledDeferredRoutes,
  2145. cancelledFetcherLoads,
  2146. deletedFetchers,
  2147. fetchLoadMatches,
  2148. fetchRedirectIds,
  2149. routesToUse,
  2150. basename,
  2151. [match.route.id, actionResult]
  2152. );
  2153. // Put all revalidating fetchers into the loading state, except for the
  2154. // current fetcher which we want to keep in it's current loading state which
  2155. // contains it's action submission info + action data
  2156. revalidatingFetchers
  2157. .filter((rf) => rf.key !== key)
  2158. .forEach((rf) => {
  2159. let staleKey = rf.key;
  2160. let existingFetcher = state.fetchers.get(staleKey);
  2161. let revalidatingFetcher = getLoadingFetcher(
  2162. undefined,
  2163. existingFetcher ? existingFetcher.data : undefined
  2164. );
  2165. state.fetchers.set(staleKey, revalidatingFetcher);
  2166. abortFetcher(staleKey);
  2167. if (rf.controller) {
  2168. fetchControllers.set(staleKey, rf.controller);
  2169. }
  2170. });
  2171. updateState({ fetchers: new Map(state.fetchers) });
  2172. let abortPendingFetchRevalidations = () =>
  2173. revalidatingFetchers.forEach((rf) => abortFetcher(rf.key));
  2174. abortController.signal.addEventListener(
  2175. "abort",
  2176. abortPendingFetchRevalidations
  2177. );
  2178. let { loaderResults, fetcherResults } =
  2179. await callLoadersAndMaybeResolveData(
  2180. state,
  2181. matches,
  2182. matchesToLoad,
  2183. revalidatingFetchers,
  2184. revalidationRequest
  2185. );
  2186. if (abortController.signal.aborted) {
  2187. return;
  2188. }
  2189. abortController.signal.removeEventListener(
  2190. "abort",
  2191. abortPendingFetchRevalidations
  2192. );
  2193. fetchReloadIds.delete(key);
  2194. fetchControllers.delete(key);
  2195. revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
  2196. let redirect = findRedirect(loaderResults);
  2197. if (redirect) {
  2198. return startRedirectNavigation(
  2199. revalidationRequest,
  2200. redirect.result,
  2201. false,
  2202. { preventScrollReset }
  2203. );
  2204. }
  2205. redirect = findRedirect(fetcherResults);
  2206. if (redirect) {
  2207. // If this redirect came from a fetcher make sure we mark it in
  2208. // fetchRedirectIds so it doesn't get revalidated on the next set of
  2209. // loader executions
  2210. fetchRedirectIds.add(redirect.key);
  2211. return startRedirectNavigation(
  2212. revalidationRequest,
  2213. redirect.result,
  2214. false,
  2215. { preventScrollReset }
  2216. );
  2217. }
  2218. // Process and commit output from loaders
  2219. let { loaderData, errors } = processLoaderData(
  2220. state,
  2221. matches,
  2222. loaderResults,
  2223. undefined,
  2224. revalidatingFetchers,
  2225. fetcherResults,
  2226. activeDeferreds
  2227. );
  2228. // Since we let revalidations complete even if the submitting fetcher was
  2229. // deleted, only put it back to idle if it hasn't been deleted
  2230. if (state.fetchers.has(key)) {
  2231. let doneFetcher = getDoneFetcher(actionResult.data);
  2232. state.fetchers.set(key, doneFetcher);
  2233. }
  2234. abortStaleFetchLoads(loadId);
  2235. // If we are currently in a navigation loading state and this fetcher is
  2236. // more recent than the navigation, we want the newer data so abort the
  2237. // navigation and complete it with the fetcher data
  2238. if (
  2239. state.navigation.state === "loading" &&
  2240. loadId > pendingNavigationLoadId
  2241. ) {
  2242. invariant(pendingAction, "Expected pending action");
  2243. pendingNavigationController && pendingNavigationController.abort();
  2244. completeNavigation(state.navigation.location, {
  2245. matches,
  2246. loaderData,
  2247. errors,
  2248. fetchers: new Map(state.fetchers),
  2249. });
  2250. } else {
  2251. // otherwise just update with the fetcher data, preserving any existing
  2252. // loaderData for loaders that did not need to reload. We have to
  2253. // manually merge here since we aren't going through completeNavigation
  2254. updateState({
  2255. errors,
  2256. loaderData: mergeLoaderData(
  2257. state.loaderData,
  2258. loaderData,
  2259. matches,
  2260. errors
  2261. ),
  2262. fetchers: new Map(state.fetchers),
  2263. });
  2264. isRevalidationRequired = false;
  2265. }
  2266. }
  2267. // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
  2268. async function handleFetcherLoader(
  2269. key: string,
  2270. routeId: string,
  2271. path: string,
  2272. match: AgnosticDataRouteMatch,
  2273. matches: AgnosticDataRouteMatch[],
  2274. isFogOfWar: boolean,
  2275. flushSync: boolean,
  2276. preventScrollReset: boolean,
  2277. submission?: Submission
  2278. ) {
  2279. let existingFetcher = state.fetchers.get(key);
  2280. updateFetcherState(
  2281. key,
  2282. getLoadingFetcher(
  2283. submission,
  2284. existingFetcher ? existingFetcher.data : undefined
  2285. ),
  2286. { flushSync }
  2287. );
  2288. let abortController = new AbortController();
  2289. let fetchRequest = createClientSideRequest(
  2290. init.history,
  2291. path,
  2292. abortController.signal
  2293. );
  2294. if (isFogOfWar) {
  2295. let discoverResult = await discoverRoutes(
  2296. matches,
  2297. new URL(fetchRequest.url).pathname,
  2298. fetchRequest.signal,
  2299. key
  2300. );
  2301. if (discoverResult.type === "aborted") {
  2302. return;
  2303. } else if (discoverResult.type === "error") {
  2304. setFetcherError(key, routeId, discoverResult.error, { flushSync });
  2305. return;
  2306. } else if (!discoverResult.matches) {
  2307. setFetcherError(
  2308. key,
  2309. routeId,
  2310. getInternalRouterError(404, { pathname: path }),
  2311. { flushSync }
  2312. );
  2313. return;
  2314. } else {
  2315. matches = discoverResult.matches;
  2316. match = getTargetMatch(matches, path);
  2317. }
  2318. }
  2319. // Call the loader for this fetcher route match
  2320. fetchControllers.set(key, abortController);
  2321. let originatingLoadId = incrementingLoadId;
  2322. let results = await callDataStrategy(
  2323. "loader",
  2324. state,
  2325. fetchRequest,
  2326. [match],
  2327. matches,
  2328. key
  2329. );
  2330. let result = results[match.route.id];
  2331. // Deferred isn't supported for fetcher loads, await everything and treat it
  2332. // as a normal load. resolveDeferredData will return undefined if this
  2333. // fetcher gets aborted, so we just leave result untouched and short circuit
  2334. // below if that happens
  2335. if (isDeferredResult(result)) {
  2336. result =
  2337. (await resolveDeferredData(result, fetchRequest.signal, true)) ||
  2338. result;
  2339. }
  2340. // We can delete this so long as we weren't aborted by our our own fetcher
  2341. // re-load which would have put _new_ controller is in fetchControllers
  2342. if (fetchControllers.get(key) === abortController) {
  2343. fetchControllers.delete(key);
  2344. }
  2345. if (fetchRequest.signal.aborted) {
  2346. return;
  2347. }
  2348. // We don't want errors bubbling up or redirects followed for unmounted
  2349. // fetchers, so short circuit here if it was removed from the UI
  2350. if (deletedFetchers.has(key)) {
  2351. updateFetcherState(key, getDoneFetcher(undefined));
  2352. return;
  2353. }
  2354. // If the loader threw a redirect Response, start a new REPLACE navigation
  2355. if (isRedirectResult(result)) {
  2356. if (pendingNavigationLoadId > originatingLoadId) {
  2357. // A new navigation was kicked off after our loader started, so that
  2358. // should take precedence over this redirect navigation
  2359. updateFetcherState(key, getDoneFetcher(undefined));
  2360. return;
  2361. } else {
  2362. fetchRedirectIds.add(key);
  2363. await startRedirectNavigation(fetchRequest, result, false, {
  2364. preventScrollReset,
  2365. });
  2366. return;
  2367. }
  2368. }
  2369. // Process any non-redirect errors thrown
  2370. if (isErrorResult(result)) {
  2371. setFetcherError(key, routeId, result.error);
  2372. return;
  2373. }
  2374. invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
  2375. // Put the fetcher back into an idle state
  2376. updateFetcherState(key, getDoneFetcher(result.data));
  2377. }
  2378. /**
  2379. * Utility function to handle redirects returned from an action or loader.
  2380. * Normally, a redirect "replaces" the navigation that triggered it. So, for
  2381. * example:
  2382. *
  2383. * - user is on /a
  2384. * - user clicks a link to /b
  2385. * - loader for /b redirects to /c
  2386. *
  2387. * In a non-JS app the browser would track the in-flight navigation to /b and
  2388. * then replace it with /c when it encountered the redirect response. In
  2389. * the end it would only ever update the URL bar with /c.
  2390. *
  2391. * In client-side routing using pushState/replaceState, we aim to emulate
  2392. * this behavior and we also do not update history until the end of the
  2393. * navigation (including processed redirects). This means that we never
  2394. * actually touch history until we've processed redirects, so we just use
  2395. * the history action from the original navigation (PUSH or REPLACE).
  2396. */
  2397. async function startRedirectNavigation(
  2398. request: Request,
  2399. redirect: RedirectResult,
  2400. isNavigation: boolean,
  2401. {
  2402. submission,
  2403. fetcherSubmission,
  2404. preventScrollReset,
  2405. replace,
  2406. }: {
  2407. submission?: Submission;
  2408. fetcherSubmission?: Submission;
  2409. preventScrollReset?: boolean;
  2410. replace?: boolean;
  2411. } = {}
  2412. ) {
  2413. if (redirect.response.headers.has("X-Remix-Revalidate")) {
  2414. isRevalidationRequired = true;
  2415. }
  2416. let location = redirect.response.headers.get("Location");
  2417. invariant(location, "Expected a Location header on the redirect Response");
  2418. location = normalizeRedirectLocation(
  2419. location,
  2420. new URL(request.url),
  2421. basename,
  2422. init.history,
  2423. );
  2424. let redirectLocation = createLocation(state.location, location, {
  2425. _isRedirect: true,
  2426. });
  2427. if (isBrowser) {
  2428. let isDocumentReload = false;
  2429. if (redirect.response.headers.has("X-Remix-Reload-Document")) {
  2430. // Hard reload if the response contained X-Remix-Reload-Document
  2431. isDocumentReload = true;
  2432. } else if (ABSOLUTE_URL_REGEX.test(location)) {
  2433. const url = init.history.createURL(location);
  2434. isDocumentReload =
  2435. // Hard reload if it's an absolute URL to a new origin
  2436. url.origin !== routerWindow.location.origin ||
  2437. // Hard reload if it's an absolute URL that does not match our basename
  2438. stripBasename(url.pathname, basename) == null;
  2439. }
  2440. if (isDocumentReload) {
  2441. if (replace) {
  2442. routerWindow.location.replace(location);
  2443. } else {
  2444. routerWindow.location.assign(location);
  2445. }
  2446. return;
  2447. }
  2448. }
  2449. // There's no need to abort on redirects, since we don't detect the
  2450. // redirect until the action/loaders have settled
  2451. pendingNavigationController = null;
  2452. let redirectHistoryAction =
  2453. replace === true || redirect.response.headers.has("X-Remix-Replace")
  2454. ? HistoryAction.Replace
  2455. : HistoryAction.Push;
  2456. // Use the incoming submission if provided, fallback on the active one in
  2457. // state.navigation
  2458. let { formMethod, formAction, formEncType } = state.navigation;
  2459. if (
  2460. !submission &&
  2461. !fetcherSubmission &&
  2462. formMethod &&
  2463. formAction &&
  2464. formEncType
  2465. ) {
  2466. submission = getSubmissionFromNavigation(state.navigation);
  2467. }
  2468. // If this was a 307/308 submission we want to preserve the HTTP method and
  2469. // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
  2470. // redirected location
  2471. let activeSubmission = submission || fetcherSubmission;
  2472. if (
  2473. redirectPreserveMethodStatusCodes.has(redirect.response.status) &&
  2474. activeSubmission &&
  2475. isMutationMethod(activeSubmission.formMethod)
  2476. ) {
  2477. await startNavigation(redirectHistoryAction, redirectLocation, {
  2478. submission: {
  2479. ...activeSubmission,
  2480. formAction: location,
  2481. },
  2482. // Preserve these flags across redirects
  2483. preventScrollReset: preventScrollReset || pendingPreventScrollReset,
  2484. enableViewTransition: isNavigation
  2485. ? pendingViewTransitionEnabled
  2486. : undefined,
  2487. });
  2488. } else {
  2489. // If we have a navigation submission, we will preserve it through the
  2490. // redirect navigation
  2491. let overrideNavigation = getLoadingNavigation(
  2492. redirectLocation,
  2493. submission
  2494. );
  2495. await startNavigation(redirectHistoryAction, redirectLocation, {
  2496. overrideNavigation,
  2497. // Send fetcher submissions through for shouldRevalidate
  2498. fetcherSubmission,
  2499. // Preserve these flags across redirects
  2500. preventScrollReset: preventScrollReset || pendingPreventScrollReset,
  2501. enableViewTransition: isNavigation
  2502. ? pendingViewTransitionEnabled
  2503. : undefined,
  2504. });
  2505. }
  2506. }
  2507. // Utility wrapper for calling dataStrategy client-side without having to
  2508. // pass around the manifest, mapRouteProperties, etc.
  2509. async function callDataStrategy(
  2510. type: "loader" | "action",
  2511. state: RouterState,
  2512. request: Request,
  2513. matchesToLoad: AgnosticDataRouteMatch[],
  2514. matches: AgnosticDataRouteMatch[],
  2515. fetcherKey: string | null
  2516. ): Promise<Record<string, DataResult>> {
  2517. let results: Record<string, DataStrategyResult>;
  2518. let dataResults: Record<string, DataResult> = {};
  2519. try {
  2520. results = await callDataStrategyImpl(
  2521. dataStrategyImpl,
  2522. type,
  2523. state,
  2524. request,
  2525. matchesToLoad,
  2526. matches,
  2527. fetcherKey,
  2528. manifest,
  2529. mapRouteProperties
  2530. );
  2531. } catch (e) {
  2532. // If the outer dataStrategy method throws, just return the error for all
  2533. // matches - and it'll naturally bubble to the root
  2534. matchesToLoad.forEach((m) => {
  2535. dataResults[m.route.id] = {
  2536. type: ResultType.error,
  2537. error: e,
  2538. };
  2539. });
  2540. return dataResults;
  2541. }
  2542. for (let [routeId, result] of Object.entries(results)) {
  2543. if (isRedirectDataStrategyResultResult(result)) {
  2544. let response = result.result as Response;
  2545. dataResults[routeId] = {
  2546. type: ResultType.redirect,
  2547. response: normalizeRelativeRoutingRedirectResponse(
  2548. response,
  2549. request,
  2550. routeId,
  2551. matches,
  2552. basename,
  2553. future.v7_relativeSplatPath
  2554. ),
  2555. };
  2556. } else {
  2557. dataResults[routeId] = await convertDataStrategyResultToDataResult(
  2558. result
  2559. );
  2560. }
  2561. }
  2562. return dataResults;
  2563. }
  2564. async function callLoadersAndMaybeResolveData(
  2565. state: RouterState,
  2566. matches: AgnosticDataRouteMatch[],
  2567. matchesToLoad: AgnosticDataRouteMatch[],
  2568. fetchersToLoad: RevalidatingFetcher[],
  2569. request: Request
  2570. ) {
  2571. let currentMatches = state.matches;
  2572. // Kick off loaders and fetchers in parallel
  2573. let loaderResultsPromise = callDataStrategy(
  2574. "loader",
  2575. state,
  2576. request,
  2577. matchesToLoad,
  2578. matches,
  2579. null
  2580. );
  2581. let fetcherResultsPromise = Promise.all(
  2582. fetchersToLoad.map(async (f) => {
  2583. if (f.matches && f.match && f.controller) {
  2584. let results = await callDataStrategy(
  2585. "loader",
  2586. state,
  2587. createClientSideRequest(init.history, f.path, f.controller.signal),
  2588. [f.match],
  2589. f.matches,
  2590. f.key
  2591. );
  2592. let result = results[f.match.route.id];
  2593. // Fetcher results are keyed by fetcher key from here on out, not routeId
  2594. return { [f.key]: result };
  2595. } else {
  2596. return Promise.resolve({
  2597. [f.key]: {
  2598. type: ResultType.error,
  2599. error: getInternalRouterError(404, {
  2600. pathname: f.path,
  2601. }),
  2602. } as ErrorResult,
  2603. });
  2604. }
  2605. })
  2606. );
  2607. let loaderResults = await loaderResultsPromise;
  2608. let fetcherResults = (await fetcherResultsPromise).reduce(
  2609. (acc, r) => Object.assign(acc, r),
  2610. {}
  2611. );
  2612. await Promise.all([
  2613. resolveNavigationDeferredResults(
  2614. matches,
  2615. loaderResults,
  2616. request.signal,
  2617. currentMatches,
  2618. state.loaderData
  2619. ),
  2620. resolveFetcherDeferredResults(matches, fetcherResults, fetchersToLoad),
  2621. ]);
  2622. return {
  2623. loaderResults,
  2624. fetcherResults,
  2625. };
  2626. }
  2627. function interruptActiveLoads() {
  2628. // Every interruption triggers a revalidation
  2629. isRevalidationRequired = true;
  2630. // Cancel pending route-level deferreds and mark cancelled routes for
  2631. // revalidation
  2632. cancelledDeferredRoutes.push(...cancelActiveDeferreds());
  2633. // Abort in-flight fetcher loads
  2634. fetchLoadMatches.forEach((_, key) => {
  2635. if (fetchControllers.has(key)) {
  2636. cancelledFetcherLoads.add(key);
  2637. }
  2638. abortFetcher(key);
  2639. });
  2640. }
  2641. function updateFetcherState(
  2642. key: string,
  2643. fetcher: Fetcher,
  2644. opts: { flushSync?: boolean } = {}
  2645. ) {
  2646. state.fetchers.set(key, fetcher);
  2647. updateState(
  2648. { fetchers: new Map(state.fetchers) },
  2649. { flushSync: (opts && opts.flushSync) === true }
  2650. );
  2651. }
  2652. function setFetcherError(
  2653. key: string,
  2654. routeId: string,
  2655. error: any,
  2656. opts: { flushSync?: boolean } = {}
  2657. ) {
  2658. let boundaryMatch = findNearestBoundary(state.matches, routeId);
  2659. deleteFetcher(key);
  2660. updateState(
  2661. {
  2662. errors: {
  2663. [boundaryMatch.route.id]: error,
  2664. },
  2665. fetchers: new Map(state.fetchers),
  2666. },
  2667. { flushSync: (opts && opts.flushSync) === true }
  2668. );
  2669. }
  2670. function getFetcher<TData = any>(key: string): Fetcher<TData> {
  2671. activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
  2672. // If this fetcher was previously marked for deletion, unmark it since we
  2673. // have a new instance
  2674. if (deletedFetchers.has(key)) {
  2675. deletedFetchers.delete(key);
  2676. }
  2677. return state.fetchers.get(key) || IDLE_FETCHER;
  2678. }
  2679. function deleteFetcher(key: string): void {
  2680. let fetcher = state.fetchers.get(key);
  2681. // Don't abort the controller if this is a deletion of a fetcher.submit()
  2682. // in it's loading phase since - we don't want to abort the corresponding
  2683. // revalidation and want them to complete and land
  2684. if (
  2685. fetchControllers.has(key) &&
  2686. !(fetcher && fetcher.state === "loading" && fetchReloadIds.has(key))
  2687. ) {
  2688. abortFetcher(key);
  2689. }
  2690. fetchLoadMatches.delete(key);
  2691. fetchReloadIds.delete(key);
  2692. fetchRedirectIds.delete(key);
  2693. // If we opted into the flag we can clear this now since we're calling
  2694. // deleteFetcher() at the end of updateState() and we've already handed the
  2695. // deleted fetcher keys off to the data layer.
  2696. // If not, we're eagerly calling deleteFetcher() and we need to keep this
  2697. // Set populated until the next updateState call, and we'll clear
  2698. // `deletedFetchers` then
  2699. if (future.v7_fetcherPersist) {
  2700. deletedFetchers.delete(key);
  2701. }
  2702. cancelledFetcherLoads.delete(key);
  2703. state.fetchers.delete(key);
  2704. }
  2705. function deleteFetcherAndUpdateState(key: string): void {
  2706. let count = (activeFetchers.get(key) || 0) - 1;
  2707. if (count <= 0) {
  2708. activeFetchers.delete(key);
  2709. deletedFetchers.add(key);
  2710. if (!future.v7_fetcherPersist) {
  2711. deleteFetcher(key);
  2712. }
  2713. } else {
  2714. activeFetchers.set(key, count);
  2715. }
  2716. updateState({ fetchers: new Map(state.fetchers) });
  2717. }
  2718. function abortFetcher(key: string) {
  2719. let controller = fetchControllers.get(key);
  2720. if (controller) {
  2721. controller.abort();
  2722. fetchControllers.delete(key);
  2723. }
  2724. }
  2725. function markFetchersDone(keys: string[]) {
  2726. for (let key of keys) {
  2727. let fetcher = getFetcher(key);
  2728. let doneFetcher = getDoneFetcher(fetcher.data);
  2729. state.fetchers.set(key, doneFetcher);
  2730. }
  2731. }
  2732. function markFetchRedirectsDone(): boolean {
  2733. let doneKeys = [];
  2734. let updatedFetchers = false;
  2735. for (let key of fetchRedirectIds) {
  2736. let fetcher = state.fetchers.get(key);
  2737. invariant(fetcher, `Expected fetcher: ${key}`);
  2738. if (fetcher.state === "loading") {
  2739. fetchRedirectIds.delete(key);
  2740. doneKeys.push(key);
  2741. updatedFetchers = true;
  2742. }
  2743. }
  2744. markFetchersDone(doneKeys);
  2745. return updatedFetchers;
  2746. }
  2747. function abortStaleFetchLoads(landedId: number): boolean {
  2748. let yeetedKeys = [];
  2749. for (let [key, id] of fetchReloadIds) {
  2750. if (id < landedId) {
  2751. let fetcher = state.fetchers.get(key);
  2752. invariant(fetcher, `Expected fetcher: ${key}`);
  2753. if (fetcher.state === "loading") {
  2754. abortFetcher(key);
  2755. fetchReloadIds.delete(key);
  2756. yeetedKeys.push(key);
  2757. }
  2758. }
  2759. }
  2760. markFetchersDone(yeetedKeys);
  2761. return yeetedKeys.length > 0;
  2762. }
  2763. function getBlocker(key: string, fn: BlockerFunction) {
  2764. let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
  2765. if (blockerFunctions.get(key) !== fn) {
  2766. blockerFunctions.set(key, fn);
  2767. }
  2768. return blocker;
  2769. }
  2770. function deleteBlocker(key: string) {
  2771. state.blockers.delete(key);
  2772. blockerFunctions.delete(key);
  2773. }
  2774. // Utility function to update blockers, ensuring valid state transitions
  2775. function updateBlocker(key: string, newBlocker: Blocker) {
  2776. let blocker = state.blockers.get(key) || IDLE_BLOCKER;
  2777. // Poor mans state machine :)
  2778. // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
  2779. invariant(
  2780. (blocker.state === "unblocked" && newBlocker.state === "blocked") ||
  2781. (blocker.state === "blocked" && newBlocker.state === "blocked") ||
  2782. (blocker.state === "blocked" && newBlocker.state === "proceeding") ||
  2783. (blocker.state === "blocked" && newBlocker.state === "unblocked") ||
  2784. (blocker.state === "proceeding" && newBlocker.state === "unblocked"),
  2785. `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
  2786. );
  2787. let blockers = new Map(state.blockers);
  2788. blockers.set(key, newBlocker);
  2789. updateState({ blockers });
  2790. }
  2791. function shouldBlockNavigation({
  2792. currentLocation,
  2793. nextLocation,
  2794. historyAction,
  2795. }: {
  2796. currentLocation: Location;
  2797. nextLocation: Location;
  2798. historyAction: HistoryAction;
  2799. }): string | undefined {
  2800. if (blockerFunctions.size === 0) {
  2801. return;
  2802. }
  2803. // We ony support a single active blocker at the moment since we don't have
  2804. // any compelling use cases for multi-blocker yet
  2805. if (blockerFunctions.size > 1) {
  2806. warning(false, "A router only supports one blocker at a time");
  2807. }
  2808. let entries = Array.from(blockerFunctions.entries());
  2809. let [blockerKey, blockerFunction] = entries[entries.length - 1];
  2810. let blocker = state.blockers.get(blockerKey);
  2811. if (blocker && blocker.state === "proceeding") {
  2812. // If the blocker is currently proceeding, we don't need to re-check
  2813. // it and can let this navigation continue
  2814. return;
  2815. }
  2816. // At this point, we know we're unblocked/blocked so we need to check the
  2817. // user-provided blocker function
  2818. if (blockerFunction({ currentLocation, nextLocation, historyAction })) {
  2819. return blockerKey;
  2820. }
  2821. }
  2822. function handleNavigational404(pathname: string) {
  2823. let error = getInternalRouterError(404, { pathname });
  2824. let routesToUse = inFlightDataRoutes || dataRoutes;
  2825. let { matches, route } = getShortCircuitMatches(routesToUse);
  2826. // Cancel all pending deferred on 404s since we don't keep any routes
  2827. cancelActiveDeferreds();
  2828. return { notFoundMatches: matches, route, error };
  2829. }
  2830. function cancelActiveDeferreds(
  2831. predicate?: (routeId: string) => boolean
  2832. ): string[] {
  2833. let cancelledRouteIds: string[] = [];
  2834. activeDeferreds.forEach((dfd, routeId) => {
  2835. if (!predicate || predicate(routeId)) {
  2836. // Cancel the deferred - but do not remove from activeDeferreds here -
  2837. // we rely on the subscribers to do that so our tests can assert proper
  2838. // cleanup via _internalActiveDeferreds
  2839. dfd.cancel();
  2840. cancelledRouteIds.push(routeId);
  2841. activeDeferreds.delete(routeId);
  2842. }
  2843. });
  2844. return cancelledRouteIds;
  2845. }
  2846. // Opt in to capturing and reporting scroll positions during navigations,
  2847. // used by the <ScrollRestoration> component
  2848. function enableScrollRestoration(
  2849. positions: Record<string, number>,
  2850. getPosition: GetScrollPositionFunction,
  2851. getKey?: GetScrollRestorationKeyFunction
  2852. ) {
  2853. savedScrollPositions = positions;
  2854. getScrollPosition = getPosition;
  2855. getScrollRestorationKey = getKey || null;
  2856. // Perform initial hydration scroll restoration, since we miss the boat on
  2857. // the initial updateState() because we've not yet rendered <ScrollRestoration/>
  2858. // and therefore have no savedScrollPositions available
  2859. if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
  2860. initialScrollRestored = true;
  2861. let y = getSavedScrollPosition(state.location, state.matches);
  2862. if (y != null) {
  2863. updateState({ restoreScrollPosition: y });
  2864. }
  2865. }
  2866. return () => {
  2867. savedScrollPositions = null;
  2868. getScrollPosition = null;
  2869. getScrollRestorationKey = null;
  2870. };
  2871. }
  2872. function getScrollKey(location: Location, matches: AgnosticDataRouteMatch[]) {
  2873. if (getScrollRestorationKey) {
  2874. let key = getScrollRestorationKey(
  2875. location,
  2876. matches.map((m) => convertRouteMatchToUiMatch(m, state.loaderData))
  2877. );
  2878. return key || location.key;
  2879. }
  2880. return location.key;
  2881. }
  2882. function saveScrollPosition(
  2883. location: Location,
  2884. matches: AgnosticDataRouteMatch[]
  2885. ): void {
  2886. if (savedScrollPositions && getScrollPosition) {
  2887. let key = getScrollKey(location, matches);
  2888. savedScrollPositions[key] = getScrollPosition();
  2889. }
  2890. }
  2891. function getSavedScrollPosition(
  2892. location: Location,
  2893. matches: AgnosticDataRouteMatch[]
  2894. ): number | null {
  2895. if (savedScrollPositions) {
  2896. let key = getScrollKey(location, matches);
  2897. let y = savedScrollPositions[key];
  2898. if (typeof y === "number") {
  2899. return y;
  2900. }
  2901. }
  2902. return null;
  2903. }
  2904. function checkFogOfWar(
  2905. matches: AgnosticDataRouteMatch[] | null,
  2906. routesToUse: AgnosticDataRouteObject[],
  2907. pathname: string
  2908. ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
  2909. if (patchRoutesOnNavigationImpl) {
  2910. if (!matches) {
  2911. let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(
  2912. routesToUse,
  2913. pathname,
  2914. basename,
  2915. true
  2916. );
  2917. return { active: true, matches: fogMatches || [] };
  2918. } else {
  2919. if (Object.keys(matches[0].params).length > 0) {
  2920. // If we matched a dynamic param or a splat, it might only be because
  2921. // we haven't yet discovered other routes that would match with a
  2922. // higher score. Call patchRoutesOnNavigation just to be sure
  2923. let partialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
  2924. routesToUse,
  2925. pathname,
  2926. basename,
  2927. true
  2928. );
  2929. return { active: true, matches: partialMatches };
  2930. }
  2931. }
  2932. }
  2933. return { active: false, matches: null };
  2934. }
  2935. type DiscoverRoutesSuccessResult = {
  2936. type: "success";
  2937. matches: AgnosticDataRouteMatch[] | null;
  2938. };
  2939. type DiscoverRoutesErrorResult = {
  2940. type: "error";
  2941. error: any;
  2942. partialMatches: AgnosticDataRouteMatch[];
  2943. };
  2944. type DiscoverRoutesAbortedResult = { type: "aborted" };
  2945. type DiscoverRoutesResult =
  2946. | DiscoverRoutesSuccessResult
  2947. | DiscoverRoutesErrorResult
  2948. | DiscoverRoutesAbortedResult;
  2949. async function discoverRoutes(
  2950. matches: AgnosticDataRouteMatch[],
  2951. pathname: string,
  2952. signal: AbortSignal,
  2953. fetcherKey?: string
  2954. ): Promise<DiscoverRoutesResult> {
  2955. if (!patchRoutesOnNavigationImpl) {
  2956. return { type: "success", matches };
  2957. }
  2958. let partialMatches: AgnosticDataRouteMatch[] | null = matches;
  2959. while (true) {
  2960. let isNonHMR = inFlightDataRoutes == null;
  2961. let routesToUse = inFlightDataRoutes || dataRoutes;
  2962. let localManifest = manifest;
  2963. try {
  2964. await patchRoutesOnNavigationImpl({
  2965. signal,
  2966. path: pathname,
  2967. matches: partialMatches,
  2968. fetcherKey,
  2969. patch: (routeId, children) => {
  2970. if (signal.aborted) return;
  2971. patchRoutesImpl(
  2972. routeId,
  2973. children,
  2974. routesToUse,
  2975. localManifest,
  2976. mapRouteProperties
  2977. );
  2978. },
  2979. });
  2980. } catch (e) {
  2981. return { type: "error", error: e, partialMatches };
  2982. } finally {
  2983. // If we are not in the middle of an HMR revalidation and we changed the
  2984. // routes, provide a new identity so when we `updateState` at the end of
  2985. // this navigation/fetch `router.routes` will be a new identity and
  2986. // trigger a re-run of memoized `router.routes` dependencies.
  2987. // HMR will already update the identity and reflow when it lands
  2988. // `inFlightDataRoutes` in `completeNavigation`
  2989. if (isNonHMR && !signal.aborted) {
  2990. dataRoutes = [...dataRoutes];
  2991. }
  2992. }
  2993. if (signal.aborted) {
  2994. return { type: "aborted" };
  2995. }
  2996. let newMatches = matchRoutes(routesToUse, pathname, basename);
  2997. if (newMatches) {
  2998. return { type: "success", matches: newMatches };
  2999. }
  3000. let newPartialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
  3001. routesToUse,
  3002. pathname,
  3003. basename,
  3004. true
  3005. );
  3006. // Avoid loops if the second pass results in the same partial matches
  3007. if (
  3008. !newPartialMatches ||
  3009. (partialMatches.length === newPartialMatches.length &&
  3010. partialMatches.every(
  3011. (m, i) => m.route.id === newPartialMatches![i].route.id
  3012. ))
  3013. ) {
  3014. return { type: "success", matches: null };
  3015. }
  3016. partialMatches = newPartialMatches;
  3017. }
  3018. }
  3019. function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
  3020. manifest = {};
  3021. inFlightDataRoutes = convertRoutesToDataRoutes(
  3022. newRoutes,
  3023. mapRouteProperties,
  3024. undefined,
  3025. manifest
  3026. );
  3027. }
  3028. function patchRoutes(
  3029. routeId: string | null,
  3030. children: AgnosticRouteObject[]
  3031. ): void {
  3032. let isNonHMR = inFlightDataRoutes == null;
  3033. let routesToUse = inFlightDataRoutes || dataRoutes;
  3034. patchRoutesImpl(
  3035. routeId,
  3036. children,
  3037. routesToUse,
  3038. manifest,
  3039. mapRouteProperties
  3040. );
  3041. // If we are not in the middle of an HMR revalidation and we changed the
  3042. // routes, provide a new identity and trigger a reflow via `updateState`
  3043. // to re-run memoized `router.routes` dependencies.
  3044. // HMR will already update the identity and reflow when it lands
  3045. // `inFlightDataRoutes` in `completeNavigation`
  3046. if (isNonHMR) {
  3047. dataRoutes = [...dataRoutes];
  3048. updateState({});
  3049. }
  3050. }
  3051. router = {
  3052. get basename() {
  3053. return basename;
  3054. },
  3055. get future() {
  3056. return future;
  3057. },
  3058. get state() {
  3059. return state;
  3060. },
  3061. get routes() {
  3062. return dataRoutes;
  3063. },
  3064. get window() {
  3065. return routerWindow;
  3066. },
  3067. initialize,
  3068. subscribe,
  3069. enableScrollRestoration,
  3070. navigate,
  3071. fetch,
  3072. revalidate,
  3073. // Passthrough to history-aware createHref used by useHref so we get proper
  3074. // hash-aware URLs in DOM paths
  3075. createHref: (to: To) => init.history.createHref(to),
  3076. encodeLocation: (to: To) => init.history.encodeLocation(to),
  3077. getFetcher,
  3078. deleteFetcher: deleteFetcherAndUpdateState,
  3079. dispose,
  3080. getBlocker,
  3081. deleteBlocker,
  3082. patchRoutes,
  3083. _internalFetchControllers: fetchControllers,
  3084. _internalActiveDeferreds: activeDeferreds,
  3085. // TODO: Remove setRoutes, it's temporary to avoid dealing with
  3086. // updating the tree while validating the update algorithm.
  3087. _internalSetRoutes,
  3088. };
  3089. return router;
  3090. }
  3091. //#endregion
  3092. ////////////////////////////////////////////////////////////////////////////////
  3093. //#region createStaticHandler
  3094. ////////////////////////////////////////////////////////////////////////////////
  3095. export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
  3096. /**
  3097. * Future flags to toggle new feature behavior
  3098. */
  3099. export interface StaticHandlerFutureConfig {
  3100. v7_relativeSplatPath: boolean;
  3101. v7_throwAbortReason: boolean;
  3102. }
  3103. export interface CreateStaticHandlerOptions {
  3104. basename?: string;
  3105. /**
  3106. * @deprecated Use `mapRouteProperties` instead
  3107. */
  3108. detectErrorBoundary?: DetectErrorBoundaryFunction;
  3109. mapRouteProperties?: MapRoutePropertiesFunction;
  3110. future?: Partial<StaticHandlerFutureConfig>;
  3111. }
  3112. export function createStaticHandler(
  3113. routes: AgnosticRouteObject[],
  3114. opts?: CreateStaticHandlerOptions
  3115. ): StaticHandler {
  3116. invariant(
  3117. routes.length > 0,
  3118. "You must provide a non-empty routes array to createStaticHandler"
  3119. );
  3120. let manifest: RouteManifest = {};
  3121. let basename = (opts ? opts.basename : null) || "/";
  3122. let mapRouteProperties: MapRoutePropertiesFunction;
  3123. if (opts?.mapRouteProperties) {
  3124. mapRouteProperties = opts.mapRouteProperties;
  3125. } else if (opts?.detectErrorBoundary) {
  3126. // If they are still using the deprecated version, wrap it with the new API
  3127. let detectErrorBoundary = opts.detectErrorBoundary;
  3128. mapRouteProperties = (route) => ({
  3129. hasErrorBoundary: detectErrorBoundary(route),
  3130. });
  3131. } else {
  3132. mapRouteProperties = defaultMapRouteProperties;
  3133. }
  3134. // Config driven behavior flags
  3135. let future: StaticHandlerFutureConfig = {
  3136. v7_relativeSplatPath: false,
  3137. v7_throwAbortReason: false,
  3138. ...(opts ? opts.future : null),
  3139. };
  3140. let dataRoutes = convertRoutesToDataRoutes(
  3141. routes,
  3142. mapRouteProperties,
  3143. undefined,
  3144. manifest
  3145. );
  3146. /**
  3147. * The query() method is intended for document requests, in which we want to
  3148. * call an optional action and potentially multiple loaders for all nested
  3149. * routes. It returns a StaticHandlerContext object, which is very similar
  3150. * to the router state (location, loaderData, actionData, errors, etc.) and
  3151. * also adds SSR-specific information such as the statusCode and headers
  3152. * from action/loaders Responses.
  3153. *
  3154. * It _should_ never throw and should report all errors through the
  3155. * returned context.errors object, properly associating errors to their error
  3156. * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
  3157. * used to emulate React error boundaries during SSr by performing a second
  3158. * pass only down to the boundaryId.
  3159. *
  3160. * The one exception where we do not return a StaticHandlerContext is when a
  3161. * redirect response is returned or thrown from any action/loader. We
  3162. * propagate that out and return the raw Response so the HTTP server can
  3163. * return it directly.
  3164. *
  3165. * - `opts.requestContext` is an optional server context that will be passed
  3166. * to actions/loaders in the `context` parameter
  3167. * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
  3168. * the bubbling of errors which allows single-fetch-type implementations
  3169. * where the client will handle the bubbling and we may need to return data
  3170. * for the handling route
  3171. */
  3172. async function query(
  3173. request: Request,
  3174. {
  3175. requestContext,
  3176. skipLoaderErrorBubbling,
  3177. dataStrategy,
  3178. }: {
  3179. requestContext?: unknown;
  3180. skipLoaderErrorBubbling?: boolean;
  3181. dataStrategy?: DataStrategyFunction;
  3182. } = {}
  3183. ): Promise<StaticHandlerContext | Response> {
  3184. let url = new URL(request.url);
  3185. let method = request.method;
  3186. let location = createLocation("", createPath(url), null, "default");
  3187. let matches = matchRoutes(dataRoutes, location, basename);
  3188. // SSR supports HEAD requests while SPA doesn't
  3189. if (!isValidMethod(method) && method !== "HEAD") {
  3190. let error = getInternalRouterError(405, { method });
  3191. let { matches: methodNotAllowedMatches, route } =
  3192. getShortCircuitMatches(dataRoutes);
  3193. return {
  3194. basename,
  3195. location,
  3196. matches: methodNotAllowedMatches,
  3197. loaderData: {},
  3198. actionData: null,
  3199. errors: {
  3200. [route.id]: error,
  3201. },
  3202. statusCode: error.status,
  3203. loaderHeaders: {},
  3204. actionHeaders: {},
  3205. activeDeferreds: null,
  3206. };
  3207. } else if (!matches) {
  3208. let error = getInternalRouterError(404, { pathname: location.pathname });
  3209. let { matches: notFoundMatches, route } =
  3210. getShortCircuitMatches(dataRoutes);
  3211. return {
  3212. basename,
  3213. location,
  3214. matches: notFoundMatches,
  3215. loaderData: {},
  3216. actionData: null,
  3217. errors: {
  3218. [route.id]: error,
  3219. },
  3220. statusCode: error.status,
  3221. loaderHeaders: {},
  3222. actionHeaders: {},
  3223. activeDeferreds: null,
  3224. };
  3225. }
  3226. let result = await queryImpl(
  3227. request,
  3228. location,
  3229. matches,
  3230. requestContext,
  3231. dataStrategy || null,
  3232. skipLoaderErrorBubbling === true,
  3233. null
  3234. );
  3235. if (isResponse(result)) {
  3236. return result;
  3237. }
  3238. // When returning StaticHandlerContext, we patch back in the location here
  3239. // since we need it for React Context. But this helps keep our submit and
  3240. // loadRouteData operating on a Request instead of a Location
  3241. return { location, basename, ...result };
  3242. }
  3243. /**
  3244. * The queryRoute() method is intended for targeted route requests, either
  3245. * for fetch ?_data requests or resource route requests. In this case, we
  3246. * are only ever calling a single action or loader, and we are returning the
  3247. * returned value directly. In most cases, this will be a Response returned
  3248. * from the action/loader, but it may be a primitive or other value as well -
  3249. * and in such cases the calling context should handle that accordingly.
  3250. *
  3251. * We do respect the throw/return differentiation, so if an action/loader
  3252. * throws, then this method will throw the value. This is important so we
  3253. * can do proper boundary identification in Remix where a thrown Response
  3254. * must go to the Catch Boundary but a returned Response is happy-path.
  3255. *
  3256. * One thing to note is that any Router-initiated Errors that make sense
  3257. * to associate with a status code will be thrown as an ErrorResponse
  3258. * instance which include the raw Error, such that the calling context can
  3259. * serialize the error as they see fit while including the proper response
  3260. * code. Examples here are 404 and 405 errors that occur prior to reaching
  3261. * any user-defined loaders.
  3262. *
  3263. * - `opts.routeId` allows you to specify the specific route handler to call.
  3264. * If not provided the handler will determine the proper route by matching
  3265. * against `request.url`
  3266. * - `opts.requestContext` is an optional server context that will be passed
  3267. * to actions/loaders in the `context` parameter
  3268. */
  3269. async function queryRoute(
  3270. request: Request,
  3271. {
  3272. routeId,
  3273. requestContext,
  3274. dataStrategy,
  3275. }: {
  3276. requestContext?: unknown;
  3277. routeId?: string;
  3278. dataStrategy?: DataStrategyFunction;
  3279. } = {}
  3280. ): Promise<any> {
  3281. let url = new URL(request.url);
  3282. let method = request.method;
  3283. let location = createLocation("", createPath(url), null, "default");
  3284. let matches = matchRoutes(dataRoutes, location, basename);
  3285. // SSR supports HEAD requests while SPA doesn't
  3286. if (!isValidMethod(method) && method !== "HEAD" && method !== "OPTIONS") {
  3287. throw getInternalRouterError(405, { method });
  3288. } else if (!matches) {
  3289. throw getInternalRouterError(404, { pathname: location.pathname });
  3290. }
  3291. let match = routeId
  3292. ? matches.find((m) => m.route.id === routeId)
  3293. : getTargetMatch(matches, location);
  3294. if (routeId && !match) {
  3295. throw getInternalRouterError(403, {
  3296. pathname: location.pathname,
  3297. routeId,
  3298. });
  3299. } else if (!match) {
  3300. // This should never hit I don't think?
  3301. throw getInternalRouterError(404, { pathname: location.pathname });
  3302. }
  3303. let result = await queryImpl(
  3304. request,
  3305. location,
  3306. matches,
  3307. requestContext,
  3308. dataStrategy || null,
  3309. false,
  3310. match
  3311. );
  3312. if (isResponse(result)) {
  3313. return result;
  3314. }
  3315. let error = result.errors ? Object.values(result.errors)[0] : undefined;
  3316. if (error !== undefined) {
  3317. // If we got back result.errors, that means the loader/action threw
  3318. // _something_ that wasn't a Response, but it's not guaranteed/required
  3319. // to be an `instanceof Error` either, so we have to use throw here to
  3320. // preserve the "error" state outside of queryImpl.
  3321. throw error;
  3322. }
  3323. // Pick off the right state value to return
  3324. if (result.actionData) {
  3325. return Object.values(result.actionData)[0];
  3326. }
  3327. if (result.loaderData) {
  3328. let data = Object.values(result.loaderData)[0];
  3329. if (result.activeDeferreds?.[match.route.id]) {
  3330. data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
  3331. }
  3332. return data;
  3333. }
  3334. return undefined;
  3335. }
  3336. async function queryImpl(
  3337. request: Request,
  3338. location: Location,
  3339. matches: AgnosticDataRouteMatch[],
  3340. requestContext: unknown,
  3341. dataStrategy: DataStrategyFunction | null,
  3342. skipLoaderErrorBubbling: boolean,
  3343. routeMatch: AgnosticDataRouteMatch | null
  3344. ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
  3345. invariant(
  3346. request.signal,
  3347. "query()/queryRoute() requests must contain an AbortController signal"
  3348. );
  3349. try {
  3350. if (isMutationMethod(request.method.toLowerCase())) {
  3351. let result = await submit(
  3352. request,
  3353. matches,
  3354. routeMatch || getTargetMatch(matches, location),
  3355. requestContext,
  3356. dataStrategy,
  3357. skipLoaderErrorBubbling,
  3358. routeMatch != null
  3359. );
  3360. return result;
  3361. }
  3362. let result = await loadRouteData(
  3363. request,
  3364. matches,
  3365. requestContext,
  3366. dataStrategy,
  3367. skipLoaderErrorBubbling,
  3368. routeMatch
  3369. );
  3370. return isResponse(result)
  3371. ? result
  3372. : {
  3373. ...result,
  3374. actionData: null,
  3375. actionHeaders: {},
  3376. };
  3377. } catch (e) {
  3378. // If the user threw/returned a Response in callLoaderOrAction for a
  3379. // `queryRoute` call, we throw the `DataStrategyResult` to bail out early
  3380. // and then return or throw the raw Response here accordingly
  3381. if (isDataStrategyResult(e) && isResponse(e.result)) {
  3382. if (e.type === ResultType.error) {
  3383. throw e.result;
  3384. }
  3385. return e.result;
  3386. }
  3387. // Redirects are always returned since they don't propagate to catch
  3388. // boundaries
  3389. if (isRedirectResponse(e)) {
  3390. return e;
  3391. }
  3392. throw e;
  3393. }
  3394. }
  3395. async function submit(
  3396. request: Request,
  3397. matches: AgnosticDataRouteMatch[],
  3398. actionMatch: AgnosticDataRouteMatch,
  3399. requestContext: unknown,
  3400. dataStrategy: DataStrategyFunction | null,
  3401. skipLoaderErrorBubbling: boolean,
  3402. isRouteRequest: boolean
  3403. ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
  3404. let result: DataResult;
  3405. if (!actionMatch.route.action && !actionMatch.route.lazy) {
  3406. let error = getInternalRouterError(405, {
  3407. method: request.method,
  3408. pathname: new URL(request.url).pathname,
  3409. routeId: actionMatch.route.id,
  3410. });
  3411. if (isRouteRequest) {
  3412. throw error;
  3413. }
  3414. result = {
  3415. type: ResultType.error,
  3416. error,
  3417. };
  3418. } else {
  3419. let results = await callDataStrategy(
  3420. "action",
  3421. request,
  3422. [actionMatch],
  3423. matches,
  3424. isRouteRequest,
  3425. requestContext,
  3426. dataStrategy
  3427. );
  3428. result = results[actionMatch.route.id];
  3429. if (request.signal.aborted) {
  3430. throwStaticHandlerAbortedError(request, isRouteRequest, future);
  3431. }
  3432. }
  3433. if (isRedirectResult(result)) {
  3434. // Uhhhh - this should never happen, we should always throw these from
  3435. // callLoaderOrAction, but the type narrowing here keeps TS happy and we
  3436. // can get back on the "throw all redirect responses" train here should
  3437. // this ever happen :/
  3438. throw new Response(null, {
  3439. status: result.response.status,
  3440. headers: {
  3441. Location: result.response.headers.get("Location")!,
  3442. },
  3443. });
  3444. }
  3445. if (isDeferredResult(result)) {
  3446. let error = getInternalRouterError(400, { type: "defer-action" });
  3447. if (isRouteRequest) {
  3448. throw error;
  3449. }
  3450. result = {
  3451. type: ResultType.error,
  3452. error,
  3453. };
  3454. }
  3455. if (isRouteRequest) {
  3456. // Note: This should only be non-Response values if we get here, since
  3457. // isRouteRequest should throw any Response received in callLoaderOrAction
  3458. if (isErrorResult(result)) {
  3459. throw result.error;
  3460. }
  3461. return {
  3462. matches: [actionMatch],
  3463. loaderData: {},
  3464. actionData: { [actionMatch.route.id]: result.data },
  3465. errors: null,
  3466. // Note: statusCode + headers are unused here since queryRoute will
  3467. // return the raw Response or value
  3468. statusCode: 200,
  3469. loaderHeaders: {},
  3470. actionHeaders: {},
  3471. activeDeferreds: null,
  3472. };
  3473. }
  3474. // Create a GET request for the loaders
  3475. let loaderRequest = new Request(request.url, {
  3476. headers: request.headers,
  3477. redirect: request.redirect,
  3478. signal: request.signal,
  3479. });
  3480. if (isErrorResult(result)) {
  3481. // Store off the pending error - we use it to determine which loaders
  3482. // to call and will commit it when we complete the navigation
  3483. let boundaryMatch = skipLoaderErrorBubbling
  3484. ? actionMatch
  3485. : findNearestBoundary(matches, actionMatch.route.id);
  3486. let context = await loadRouteData(
  3487. loaderRequest,
  3488. matches,
  3489. requestContext,
  3490. dataStrategy,
  3491. skipLoaderErrorBubbling,
  3492. null,
  3493. [boundaryMatch.route.id, result]
  3494. );
  3495. // action status codes take precedence over loader status codes
  3496. return {
  3497. ...context,
  3498. statusCode: isRouteErrorResponse(result.error)
  3499. ? result.error.status
  3500. : result.statusCode != null
  3501. ? result.statusCode
  3502. : 500,
  3503. actionData: null,
  3504. actionHeaders: {
  3505. ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
  3506. },
  3507. };
  3508. }
  3509. let context = await loadRouteData(
  3510. loaderRequest,
  3511. matches,
  3512. requestContext,
  3513. dataStrategy,
  3514. skipLoaderErrorBubbling,
  3515. null
  3516. );
  3517. return {
  3518. ...context,
  3519. actionData: {
  3520. [actionMatch.route.id]: result.data,
  3521. },
  3522. // action status codes take precedence over loader status codes
  3523. ...(result.statusCode ? { statusCode: result.statusCode } : {}),
  3524. actionHeaders: result.headers
  3525. ? { [actionMatch.route.id]: result.headers }
  3526. : {},
  3527. };
  3528. }
  3529. async function loadRouteData(
  3530. request: Request,
  3531. matches: AgnosticDataRouteMatch[],
  3532. requestContext: unknown,
  3533. dataStrategy: DataStrategyFunction | null,
  3534. skipLoaderErrorBubbling: boolean,
  3535. routeMatch: AgnosticDataRouteMatch | null,
  3536. pendingActionResult?: PendingActionResult
  3537. ): Promise<
  3538. | Omit<
  3539. StaticHandlerContext,
  3540. "location" | "basename" | "actionData" | "actionHeaders"
  3541. >
  3542. | Response
  3543. > {
  3544. let isRouteRequest = routeMatch != null;
  3545. // Short circuit if we have no loaders to run (queryRoute())
  3546. if (
  3547. isRouteRequest &&
  3548. !routeMatch?.route.loader &&
  3549. !routeMatch?.route.lazy
  3550. ) {
  3551. throw getInternalRouterError(400, {
  3552. method: request.method,
  3553. pathname: new URL(request.url).pathname,
  3554. routeId: routeMatch?.route.id,
  3555. });
  3556. }
  3557. let requestMatches = routeMatch
  3558. ? [routeMatch]
  3559. : pendingActionResult && isErrorResult(pendingActionResult[1])
  3560. ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0])
  3561. : matches;
  3562. let matchesToLoad = requestMatches.filter(
  3563. (m) => m.route.loader || m.route.lazy
  3564. );
  3565. // Short circuit if we have no loaders to run (query())
  3566. if (matchesToLoad.length === 0) {
  3567. return {
  3568. matches,
  3569. // Add a null for all matched routes for proper revalidation on the client
  3570. loaderData: matches.reduce(
  3571. (acc, m) => Object.assign(acc, { [m.route.id]: null }),
  3572. {}
  3573. ),
  3574. errors:
  3575. pendingActionResult && isErrorResult(pendingActionResult[1])
  3576. ? {
  3577. [pendingActionResult[0]]: pendingActionResult[1].error,
  3578. }
  3579. : null,
  3580. statusCode: 200,
  3581. loaderHeaders: {},
  3582. activeDeferreds: null,
  3583. };
  3584. }
  3585. let results = await callDataStrategy(
  3586. "loader",
  3587. request,
  3588. matchesToLoad,
  3589. matches,
  3590. isRouteRequest,
  3591. requestContext,
  3592. dataStrategy
  3593. );
  3594. if (request.signal.aborted) {
  3595. throwStaticHandlerAbortedError(request, isRouteRequest, future);
  3596. }
  3597. // Process and commit output from loaders
  3598. let activeDeferreds = new Map<string, DeferredData>();
  3599. let context = processRouteLoaderData(
  3600. matches,
  3601. results,
  3602. pendingActionResult,
  3603. activeDeferreds,
  3604. skipLoaderErrorBubbling
  3605. );
  3606. // Add a null for any non-loader matches for proper revalidation on the client
  3607. let executedLoaders = new Set<string>(
  3608. matchesToLoad.map((match) => match.route.id)
  3609. );
  3610. matches.forEach((match) => {
  3611. if (!executedLoaders.has(match.route.id)) {
  3612. context.loaderData[match.route.id] = null;
  3613. }
  3614. });
  3615. return {
  3616. ...context,
  3617. matches,
  3618. activeDeferreds:
  3619. activeDeferreds.size > 0
  3620. ? Object.fromEntries(activeDeferreds.entries())
  3621. : null,
  3622. };
  3623. }
  3624. // Utility wrapper for calling dataStrategy server-side without having to
  3625. // pass around the manifest, mapRouteProperties, etc.
  3626. async function callDataStrategy(
  3627. type: "loader" | "action",
  3628. request: Request,
  3629. matchesToLoad: AgnosticDataRouteMatch[],
  3630. matches: AgnosticDataRouteMatch[],
  3631. isRouteRequest: boolean,
  3632. requestContext: unknown,
  3633. dataStrategy: DataStrategyFunction | null
  3634. ): Promise<Record<string, DataResult>> {
  3635. let results = await callDataStrategyImpl(
  3636. dataStrategy || defaultDataStrategy,
  3637. type,
  3638. null,
  3639. request,
  3640. matchesToLoad,
  3641. matches,
  3642. null,
  3643. manifest,
  3644. mapRouteProperties,
  3645. requestContext
  3646. );
  3647. let dataResults: Record<string, DataResult> = {};
  3648. await Promise.all(
  3649. matches.map(async (match) => {
  3650. if (!(match.route.id in results)) {
  3651. return;
  3652. }
  3653. let result = results[match.route.id];
  3654. if (isRedirectDataStrategyResultResult(result)) {
  3655. let response = result.result as Response;
  3656. // Throw redirects and let the server handle them with an HTTP redirect
  3657. throw normalizeRelativeRoutingRedirectResponse(
  3658. response,
  3659. request,
  3660. match.route.id,
  3661. matches,
  3662. basename,
  3663. future.v7_relativeSplatPath
  3664. );
  3665. }
  3666. if (isResponse(result.result) && isRouteRequest) {
  3667. // For SSR single-route requests, we want to hand Responses back
  3668. // directly without unwrapping
  3669. throw result;
  3670. }
  3671. dataResults[match.route.id] =
  3672. await convertDataStrategyResultToDataResult(result);
  3673. })
  3674. );
  3675. return dataResults;
  3676. }
  3677. return {
  3678. dataRoutes,
  3679. query,
  3680. queryRoute,
  3681. };
  3682. }
  3683. //#endregion
  3684. ////////////////////////////////////////////////////////////////////////////////
  3685. //#region Helpers
  3686. ////////////////////////////////////////////////////////////////////////////////
  3687. /**
  3688. * Given an existing StaticHandlerContext and an error thrown at render time,
  3689. * provide an updated StaticHandlerContext suitable for a second SSR render
  3690. */
  3691. export function getStaticContextFromError(
  3692. routes: AgnosticDataRouteObject[],
  3693. context: StaticHandlerContext,
  3694. error: any
  3695. ) {
  3696. let newContext: StaticHandlerContext = {
  3697. ...context,
  3698. statusCode: isRouteErrorResponse(error) ? error.status : 500,
  3699. errors: {
  3700. [context._deepestRenderedBoundaryId || routes[0].id]: error,
  3701. },
  3702. };
  3703. return newContext;
  3704. }
  3705. function throwStaticHandlerAbortedError(
  3706. request: Request,
  3707. isRouteRequest: boolean,
  3708. future: StaticHandlerFutureConfig
  3709. ) {
  3710. if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
  3711. throw request.signal.reason;
  3712. }
  3713. let method = isRouteRequest ? "queryRoute" : "query";
  3714. throw new Error(`${method}() call aborted: ${request.method} ${request.url}`);
  3715. }
  3716. function isSubmissionNavigation(
  3717. opts: BaseNavigateOrFetchOptions
  3718. ): opts is SubmissionNavigateOptions {
  3719. return (
  3720. opts != null &&
  3721. (("formData" in opts && opts.formData != null) ||
  3722. ("body" in opts && opts.body !== undefined))
  3723. );
  3724. }
  3725. function normalizeTo(
  3726. location: Path,
  3727. matches: AgnosticDataRouteMatch[],
  3728. basename: string,
  3729. prependBasename: boolean,
  3730. to: To | null,
  3731. v7_relativeSplatPath: boolean,
  3732. fromRouteId?: string,
  3733. relative?: RelativeRoutingType
  3734. ) {
  3735. let contextualMatches: AgnosticDataRouteMatch[];
  3736. let activeRouteMatch: AgnosticDataRouteMatch | undefined;
  3737. if (fromRouteId) {
  3738. // Grab matches up to the calling route so our route-relative logic is
  3739. // relative to the correct source route
  3740. contextualMatches = [];
  3741. for (let match of matches) {
  3742. contextualMatches.push(match);
  3743. if (match.route.id === fromRouteId) {
  3744. activeRouteMatch = match;
  3745. break;
  3746. }
  3747. }
  3748. } else {
  3749. contextualMatches = matches;
  3750. activeRouteMatch = matches[matches.length - 1];
  3751. }
  3752. // Resolve the relative path
  3753. let path = resolveTo(
  3754. to ? to : ".",
  3755. getResolveToMatches(contextualMatches, v7_relativeSplatPath),
  3756. stripBasename(location.pathname, basename) || location.pathname,
  3757. relative === "path"
  3758. );
  3759. // When `to` is not specified we inherit search/hash from the current
  3760. // location, unlike when to="." and we just inherit the path.
  3761. // See https://github.com/remix-run/remix/issues/927
  3762. if (to == null) {
  3763. path.search = location.search;
  3764. path.hash = location.hash;
  3765. }
  3766. // Account for `?index` params when routing to the current location
  3767. if ((to == null || to === "" || to === ".") && activeRouteMatch) {
  3768. let nakedIndex = hasNakedIndexQuery(path.search);
  3769. if (activeRouteMatch.route.index && !nakedIndex) {
  3770. // Add one when we're targeting an index route
  3771. path.search = path.search
  3772. ? path.search.replace(/^\?/, "?index&")
  3773. : "?index";
  3774. } else if (!activeRouteMatch.route.index && nakedIndex) {
  3775. // Remove existing ones when we're not
  3776. let params = new URLSearchParams(path.search);
  3777. let indexValues = params.getAll("index");
  3778. params.delete("index");
  3779. indexValues.filter((v) => v).forEach((v) => params.append("index", v));
  3780. let qs = params.toString();
  3781. path.search = qs ? `?${qs}` : "";
  3782. }
  3783. }
  3784. // If we're operating within a basename, prepend it to the pathname. If
  3785. // this is a root navigation, then just use the raw basename which allows
  3786. // the basename to have full control over the presence of a trailing slash
  3787. // on root actions
  3788. if (prependBasename && basename !== "/") {
  3789. path.pathname =
  3790. path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
  3791. }
  3792. return createPath(path);
  3793. }
  3794. // Normalize navigation options by converting formMethod=GET formData objects to
  3795. // URLSearchParams so they behave identically to links with query params
  3796. function normalizeNavigateOptions(
  3797. normalizeFormMethod: boolean,
  3798. isFetcher: boolean,
  3799. path: string,
  3800. opts?: BaseNavigateOrFetchOptions
  3801. ): {
  3802. path: string;
  3803. submission?: Submission;
  3804. error?: ErrorResponseImpl;
  3805. } {
  3806. // Return location verbatim on non-submission navigations
  3807. if (!opts || !isSubmissionNavigation(opts)) {
  3808. return { path };
  3809. }
  3810. if (opts.formMethod && !isValidMethod(opts.formMethod)) {
  3811. return {
  3812. path,
  3813. error: getInternalRouterError(405, { method: opts.formMethod }),
  3814. };
  3815. }
  3816. let getInvalidBodyError = () => ({
  3817. path,
  3818. error: getInternalRouterError(400, { type: "invalid-body" }),
  3819. });
  3820. // Create a Submission on non-GET navigations
  3821. let rawFormMethod = opts.formMethod || "get";
  3822. let formMethod = normalizeFormMethod
  3823. ? (rawFormMethod.toUpperCase() as V7_FormMethod)
  3824. : (rawFormMethod.toLowerCase() as FormMethod);
  3825. let formAction = stripHashFromPath(path);
  3826. if (opts.body !== undefined) {
  3827. if (opts.formEncType === "text/plain") {
  3828. // text only support POST/PUT/PATCH/DELETE submissions
  3829. if (!isMutationMethod(formMethod)) {
  3830. return getInvalidBodyError();
  3831. }
  3832. let text =
  3833. typeof opts.body === "string"
  3834. ? opts.body
  3835. : opts.body instanceof FormData ||
  3836. opts.body instanceof URLSearchParams
  3837. ? // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
  3838. Array.from(opts.body.entries()).reduce(
  3839. (acc, [name, value]) => `${acc}${name}=${value}\n`,
  3840. ""
  3841. )
  3842. : String(opts.body);
  3843. return {
  3844. path,
  3845. submission: {
  3846. formMethod,
  3847. formAction,
  3848. formEncType: opts.formEncType,
  3849. formData: undefined,
  3850. json: undefined,
  3851. text,
  3852. },
  3853. };
  3854. } else if (opts.formEncType === "application/json") {
  3855. // json only supports POST/PUT/PATCH/DELETE submissions
  3856. if (!isMutationMethod(formMethod)) {
  3857. return getInvalidBodyError();
  3858. }
  3859. try {
  3860. let json =
  3861. typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
  3862. return {
  3863. path,
  3864. submission: {
  3865. formMethod,
  3866. formAction,
  3867. formEncType: opts.formEncType,
  3868. formData: undefined,
  3869. json,
  3870. text: undefined,
  3871. },
  3872. };
  3873. } catch (e) {
  3874. return getInvalidBodyError();
  3875. }
  3876. }
  3877. }
  3878. invariant(
  3879. typeof FormData === "function",
  3880. "FormData is not available in this environment"
  3881. );
  3882. let searchParams: URLSearchParams;
  3883. let formData: FormData;
  3884. if (opts.formData) {
  3885. searchParams = convertFormDataToSearchParams(opts.formData);
  3886. formData = opts.formData;
  3887. } else if (opts.body instanceof FormData) {
  3888. searchParams = convertFormDataToSearchParams(opts.body);
  3889. formData = opts.body;
  3890. } else if (opts.body instanceof URLSearchParams) {
  3891. searchParams = opts.body;
  3892. formData = convertSearchParamsToFormData(searchParams);
  3893. } else if (opts.body == null) {
  3894. searchParams = new URLSearchParams();
  3895. formData = new FormData();
  3896. } else {
  3897. try {
  3898. searchParams = new URLSearchParams(opts.body);
  3899. formData = convertSearchParamsToFormData(searchParams);
  3900. } catch (e) {
  3901. return getInvalidBodyError();
  3902. }
  3903. }
  3904. let submission: Submission = {
  3905. formMethod,
  3906. formAction,
  3907. formEncType:
  3908. (opts && opts.formEncType) || "application/x-www-form-urlencoded",
  3909. formData,
  3910. json: undefined,
  3911. text: undefined,
  3912. };
  3913. if (isMutationMethod(submission.formMethod)) {
  3914. return { path, submission };
  3915. }
  3916. // Flatten submission onto URLSearchParams for GET submissions
  3917. let parsedPath = parsePath(path);
  3918. // On GET navigation submissions we can drop the ?index param from the
  3919. // resulting location since all loaders will run. But fetcher GET submissions
  3920. // only run a single loader so we need to preserve any incoming ?index params
  3921. if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
  3922. searchParams.append("index", "");
  3923. }
  3924. parsedPath.search = `?${searchParams}`;
  3925. return { path: createPath(parsedPath), submission };
  3926. }
  3927. // Filter out all routes at/below any caught error as they aren't going to
  3928. // render so we don't need to load them
  3929. function getLoaderMatchesUntilBoundary(
  3930. matches: AgnosticDataRouteMatch[],
  3931. boundaryId: string,
  3932. includeBoundary = false
  3933. ) {
  3934. let index = matches.findIndex((m) => m.route.id === boundaryId);
  3935. if (index >= 0) {
  3936. return matches.slice(0, includeBoundary ? index + 1 : index);
  3937. }
  3938. return matches;
  3939. }
  3940. function getMatchesToLoad(
  3941. history: History,
  3942. state: RouterState,
  3943. matches: AgnosticDataRouteMatch[],
  3944. submission: Submission | undefined,
  3945. location: Location,
  3946. initialHydration: boolean,
  3947. skipActionErrorRevalidation: boolean,
  3948. isRevalidationRequired: boolean,
  3949. cancelledDeferredRoutes: string[],
  3950. cancelledFetcherLoads: Set<string>,
  3951. deletedFetchers: Set<string>,
  3952. fetchLoadMatches: Map<string, FetchLoadMatch>,
  3953. fetchRedirectIds: Set<string>,
  3954. routesToUse: AgnosticDataRouteObject[],
  3955. basename: string | undefined,
  3956. pendingActionResult?: PendingActionResult
  3957. ): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
  3958. let actionResult = pendingActionResult
  3959. ? isErrorResult(pendingActionResult[1])
  3960. ? pendingActionResult[1].error
  3961. : pendingActionResult[1].data
  3962. : undefined;
  3963. let currentUrl = history.createURL(state.location);
  3964. let nextUrl = history.createURL(location);
  3965. // Pick navigation matches that are net-new or qualify for revalidation
  3966. let boundaryMatches = matches;
  3967. if (initialHydration && state.errors) {
  3968. // On initial hydration, only consider matches up to _and including_ the boundary.
  3969. // This is inclusive to handle cases where a server loader ran successfully,
  3970. // a child server loader bubbled up to this route, but this route has
  3971. // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
  3972. // we have a complete version of `loaderData`
  3973. boundaryMatches = getLoaderMatchesUntilBoundary(
  3974. matches,
  3975. Object.keys(state.errors)[0],
  3976. true
  3977. );
  3978. } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {
  3979. // If an action threw an error, we call loaders up to, but not including the
  3980. // boundary
  3981. boundaryMatches = getLoaderMatchesUntilBoundary(
  3982. matches,
  3983. pendingActionResult[0]
  3984. );
  3985. }
  3986. // Don't revalidate loaders by default after action 4xx/5xx responses
  3987. // when the flag is enabled. They can still opt-into revalidation via
  3988. // `shouldRevalidate` via `actionResult`
  3989. let actionStatus = pendingActionResult
  3990. ? pendingActionResult[1].statusCode
  3991. : undefined;
  3992. let shouldSkipRevalidation =
  3993. skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
  3994. let navigationMatches = boundaryMatches.filter((match, index) => {
  3995. let { route } = match;
  3996. if (route.lazy) {
  3997. // We haven't loaded this route yet so we don't know if it's got a loader!
  3998. return true;
  3999. }
  4000. if (route.loader == null) {
  4001. return false;
  4002. }
  4003. if (initialHydration) {
  4004. return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
  4005. }
  4006. // Always call the loader on new route instances and pending defer cancellations
  4007. if (
  4008. isNewLoader(state.loaderData, state.matches[index], match) ||
  4009. cancelledDeferredRoutes.some((id) => id === match.route.id)
  4010. ) {
  4011. return true;
  4012. }
  4013. // This is the default implementation for when we revalidate. If the route
  4014. // provides it's own implementation, then we give them full control but
  4015. // provide this value so they can leverage it if needed after they check
  4016. // their own specific use cases
  4017. let currentRouteMatch = state.matches[index];
  4018. let nextRouteMatch = match;
  4019. return shouldRevalidateLoader(match, {
  4020. currentUrl,
  4021. currentParams: currentRouteMatch.params,
  4022. nextUrl,
  4023. nextParams: nextRouteMatch.params,
  4024. ...submission,
  4025. actionResult,
  4026. actionStatus,
  4027. defaultShouldRevalidate: shouldSkipRevalidation
  4028. ? false
  4029. : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
  4030. isRevalidationRequired ||
  4031. currentUrl.pathname + currentUrl.search ===
  4032. nextUrl.pathname + nextUrl.search ||
  4033. // Search params affect all loaders
  4034. currentUrl.search !== nextUrl.search ||
  4035. isNewRouteInstance(currentRouteMatch, nextRouteMatch),
  4036. });
  4037. });
  4038. // Pick fetcher.loads that need to be revalidated
  4039. let revalidatingFetchers: RevalidatingFetcher[] = [];
  4040. fetchLoadMatches.forEach((f, key) => {
  4041. // Don't revalidate:
  4042. // - on initial hydration (shouldn't be any fetchers then anyway)
  4043. // - if fetcher won't be present in the subsequent render
  4044. // - no longer matches the URL (v7_fetcherPersist=false)
  4045. // - was unmounted but persisted due to v7_fetcherPersist=true
  4046. if (
  4047. initialHydration ||
  4048. !matches.some((m) => m.route.id === f.routeId) ||
  4049. deletedFetchers.has(key)
  4050. ) {
  4051. return;
  4052. }
  4053. let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
  4054. // If the fetcher path no longer matches, push it in with null matches so
  4055. // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is
  4056. // currently only a use-case for Remix HMR where the route tree can change
  4057. // at runtime and remove a route previously loaded via a fetcher
  4058. if (!fetcherMatches) {
  4059. revalidatingFetchers.push({
  4060. key,
  4061. routeId: f.routeId,
  4062. path: f.path,
  4063. matches: null,
  4064. match: null,
  4065. controller: null,
  4066. });
  4067. return;
  4068. }
  4069. // Revalidating fetchers are decoupled from the route matches since they
  4070. // load from a static href. They revalidate based on explicit revalidation
  4071. // (submission, useRevalidator, or X-Remix-Revalidate)
  4072. let fetcher = state.fetchers.get(key);
  4073. let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
  4074. let shouldRevalidate = false;
  4075. if (fetchRedirectIds.has(key)) {
  4076. // Never trigger a revalidation of an actively redirecting fetcher
  4077. shouldRevalidate = false;
  4078. } else if (cancelledFetcherLoads.has(key)) {
  4079. // Always mark for revalidation if the fetcher was cancelled
  4080. cancelledFetcherLoads.delete(key);
  4081. shouldRevalidate = true;
  4082. } else if (
  4083. fetcher &&
  4084. fetcher.state !== "idle" &&
  4085. fetcher.data === undefined
  4086. ) {
  4087. // If the fetcher hasn't ever completed loading yet, then this isn't a
  4088. // revalidation, it would just be a brand new load if an explicit
  4089. // revalidation is required
  4090. shouldRevalidate = isRevalidationRequired;
  4091. } else {
  4092. // Otherwise fall back on any user-defined shouldRevalidate, defaulting
  4093. // to explicit revalidations only
  4094. shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
  4095. currentUrl,
  4096. currentParams: state.matches[state.matches.length - 1].params,
  4097. nextUrl,
  4098. nextParams: matches[matches.length - 1].params,
  4099. ...submission,
  4100. actionResult,
  4101. actionStatus,
  4102. defaultShouldRevalidate: shouldSkipRevalidation
  4103. ? false
  4104. : isRevalidationRequired,
  4105. });
  4106. }
  4107. if (shouldRevalidate) {
  4108. revalidatingFetchers.push({
  4109. key,
  4110. routeId: f.routeId,
  4111. path: f.path,
  4112. matches: fetcherMatches,
  4113. match: fetcherMatch,
  4114. controller: new AbortController(),
  4115. });
  4116. }
  4117. });
  4118. return [navigationMatches, revalidatingFetchers];
  4119. }
  4120. function shouldLoadRouteOnHydration(
  4121. route: AgnosticDataRouteObject,
  4122. loaderData: RouteData | null | undefined,
  4123. errors: RouteData | null | undefined
  4124. ) {
  4125. // We dunno if we have a loader - gotta find out!
  4126. if (route.lazy) {
  4127. return true;
  4128. }
  4129. // No loader, nothing to initialize
  4130. if (!route.loader) {
  4131. return false;
  4132. }
  4133. let hasData = loaderData != null && loaderData[route.id] !== undefined;
  4134. let hasError = errors != null && errors[route.id] !== undefined;
  4135. // Don't run if we error'd during SSR
  4136. if (!hasData && hasError) {
  4137. return false;
  4138. }
  4139. // Explicitly opting-in to running on hydration
  4140. if (typeof route.loader === "function" && route.loader.hydrate === true) {
  4141. return true;
  4142. }
  4143. // Otherwise, run if we're not yet initialized with anything
  4144. return !hasData && !hasError;
  4145. }
  4146. function isNewLoader(
  4147. currentLoaderData: RouteData,
  4148. currentMatch: AgnosticDataRouteMatch,
  4149. match: AgnosticDataRouteMatch
  4150. ) {
  4151. let isNew =
  4152. // [a] -> [a, b]
  4153. !currentMatch ||
  4154. // [a, b] -> [a, c]
  4155. match.route.id !== currentMatch.route.id;
  4156. // Handle the case that we don't have data for a re-used route, potentially
  4157. // from a prior error or from a cancelled pending deferred
  4158. let isMissingData = currentLoaderData[match.route.id] === undefined;
  4159. // Always load if this is a net-new route or we don't yet have data
  4160. return isNew || isMissingData;
  4161. }
  4162. function isNewRouteInstance(
  4163. currentMatch: AgnosticDataRouteMatch,
  4164. match: AgnosticDataRouteMatch
  4165. ) {
  4166. let currentPath = currentMatch.route.path;
  4167. return (
  4168. // param change for this match, /users/123 -> /users/456
  4169. currentMatch.pathname !== match.pathname ||
  4170. // splat param changed, which is not present in match.path
  4171. // e.g. /files/images/avatar.jpg -> files/finances.xls
  4172. (currentPath != null &&
  4173. currentPath.endsWith("*") &&
  4174. currentMatch.params["*"] !== match.params["*"])
  4175. );
  4176. }
  4177. function shouldRevalidateLoader(
  4178. loaderMatch: AgnosticDataRouteMatch,
  4179. arg: ShouldRevalidateFunctionArgs
  4180. ) {
  4181. if (loaderMatch.route.shouldRevalidate) {
  4182. let routeChoice = loaderMatch.route.shouldRevalidate(arg);
  4183. if (typeof routeChoice === "boolean") {
  4184. return routeChoice;
  4185. }
  4186. }
  4187. return arg.defaultShouldRevalidate;
  4188. }
  4189. function patchRoutesImpl(
  4190. routeId: string | null,
  4191. children: AgnosticRouteObject[],
  4192. routesToUse: AgnosticDataRouteObject[],
  4193. manifest: RouteManifest,
  4194. mapRouteProperties: MapRoutePropertiesFunction
  4195. ) {
  4196. let childrenToPatch: AgnosticDataRouteObject[];
  4197. if (routeId) {
  4198. let route = manifest[routeId];
  4199. invariant(
  4200. route,
  4201. `No route found to patch children into: routeId = ${routeId}`
  4202. );
  4203. if (!route.children) {
  4204. route.children = [];
  4205. }
  4206. childrenToPatch = route.children;
  4207. } else {
  4208. childrenToPatch = routesToUse;
  4209. }
  4210. // Don't patch in routes we already know about so that `patch` is idempotent
  4211. // to simplify user-land code. This is useful because we re-call the
  4212. // `patchRoutesOnNavigation` function for matched routes with params.
  4213. let uniqueChildren = children.filter(
  4214. (newRoute) =>
  4215. !childrenToPatch.some((existingRoute) =>
  4216. isSameRoute(newRoute, existingRoute)
  4217. )
  4218. );
  4219. let newRoutes = convertRoutesToDataRoutes(
  4220. uniqueChildren,
  4221. mapRouteProperties,
  4222. [routeId || "_", "patch", String(childrenToPatch?.length || "0")],
  4223. manifest
  4224. );
  4225. childrenToPatch.push(...newRoutes);
  4226. }
  4227. function isSameRoute(
  4228. newRoute: AgnosticRouteObject,
  4229. existingRoute: AgnosticRouteObject
  4230. ): boolean {
  4231. // Most optimal check is by id
  4232. if (
  4233. "id" in newRoute &&
  4234. "id" in existingRoute &&
  4235. newRoute.id === existingRoute.id
  4236. ) {
  4237. return true;
  4238. }
  4239. // Second is by pathing differences
  4240. if (
  4241. !(
  4242. newRoute.index === existingRoute.index &&
  4243. newRoute.path === existingRoute.path &&
  4244. newRoute.caseSensitive === existingRoute.caseSensitive
  4245. )
  4246. ) {
  4247. return false;
  4248. }
  4249. // Pathless layout routes are trickier since we need to check children.
  4250. // If they have no children then they're the same as far as we can tell
  4251. if (
  4252. (!newRoute.children || newRoute.children.length === 0) &&
  4253. (!existingRoute.children || existingRoute.children.length === 0)
  4254. ) {
  4255. return true;
  4256. }
  4257. // Otherwise, we look to see if every child in the new route is already
  4258. // represented in the existing route's children
  4259. return newRoute.children!.every((aChild, i) =>
  4260. existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild))
  4261. );
  4262. }
  4263. /**
  4264. * Execute route.lazy() methods to lazily load route modules (loader, action,
  4265. * shouldRevalidate) and update the routeManifest in place which shares objects
  4266. * with dataRoutes so those get updated as well.
  4267. */
  4268. async function loadLazyRouteModule(
  4269. route: AgnosticDataRouteObject,
  4270. mapRouteProperties: MapRoutePropertiesFunction,
  4271. manifest: RouteManifest
  4272. ) {
  4273. if (!route.lazy) {
  4274. return;
  4275. }
  4276. let lazyRoute = await route.lazy();
  4277. // If the lazy route function was executed and removed by another parallel
  4278. // call then we can return - first lazy() to finish wins because the return
  4279. // value of lazy is expected to be static
  4280. if (!route.lazy) {
  4281. return;
  4282. }
  4283. let routeToUpdate = manifest[route.id];
  4284. invariant(routeToUpdate, "No route found in manifest");
  4285. // Update the route in place. This should be safe because there's no way
  4286. // we could yet be sitting on this route as we can't get there without
  4287. // resolving lazy() first.
  4288. //
  4289. // This is different than the HMR "update" use-case where we may actively be
  4290. // on the route being updated. The main concern boils down to "does this
  4291. // mutation affect any ongoing navigations or any current state.matches
  4292. // values?". If not, it should be safe to update in place.
  4293. let routeUpdates: Record<string, any> = {};
  4294. for (let lazyRouteProperty in lazyRoute) {
  4295. let staticRouteValue =
  4296. routeToUpdate[lazyRouteProperty as keyof typeof routeToUpdate];
  4297. let isPropertyStaticallyDefined =
  4298. staticRouteValue !== undefined &&
  4299. // This property isn't static since it should always be updated based
  4300. // on the route updates
  4301. lazyRouteProperty !== "hasErrorBoundary";
  4302. warning(
  4303. !isPropertyStaticallyDefined,
  4304. `Route "${routeToUpdate.id}" has a static property "${lazyRouteProperty}" ` +
  4305. `defined but its lazy function is also returning a value for this property. ` +
  4306. `The lazy route property "${lazyRouteProperty}" will be ignored.`
  4307. );
  4308. if (
  4309. !isPropertyStaticallyDefined &&
  4310. !immutableRouteKeys.has(lazyRouteProperty as ImmutableRouteKey)
  4311. ) {
  4312. routeUpdates[lazyRouteProperty] =
  4313. lazyRoute[lazyRouteProperty as keyof typeof lazyRoute];
  4314. }
  4315. }
  4316. // Mutate the route with the provided updates. Do this first so we pass
  4317. // the updated version to mapRouteProperties
  4318. Object.assign(routeToUpdate, routeUpdates);
  4319. // Mutate the `hasErrorBoundary` property on the route based on the route
  4320. // updates and remove the `lazy` function so we don't resolve the lazy
  4321. // route again.
  4322. Object.assign(routeToUpdate, {
  4323. // To keep things framework agnostic, we use the provided
  4324. // `mapRouteProperties` (or wrapped `detectErrorBoundary`) function to
  4325. // set the framework-aware properties (`element`/`hasErrorBoundary`) since
  4326. // the logic will differ between frameworks.
  4327. ...mapRouteProperties(routeToUpdate),
  4328. lazy: undefined,
  4329. });
  4330. }
  4331. // Default implementation of `dataStrategy` which fetches all loaders in parallel
  4332. async function defaultDataStrategy({
  4333. matches,
  4334. }: DataStrategyFunctionArgs): ReturnType<DataStrategyFunction> {
  4335. let matchesToLoad = matches.filter((m) => m.shouldLoad);
  4336. let results = await Promise.all(matchesToLoad.map((m) => m.resolve()));
  4337. return results.reduce(
  4338. (acc, result, i) =>
  4339. Object.assign(acc, { [matchesToLoad[i].route.id]: result }),
  4340. {}
  4341. );
  4342. }
  4343. async function callDataStrategyImpl(
  4344. dataStrategyImpl: DataStrategyFunction,
  4345. type: "loader" | "action",
  4346. state: RouterState | null,
  4347. request: Request,
  4348. matchesToLoad: AgnosticDataRouteMatch[],
  4349. matches: AgnosticDataRouteMatch[],
  4350. fetcherKey: string | null,
  4351. manifest: RouteManifest,
  4352. mapRouteProperties: MapRoutePropertiesFunction,
  4353. requestContext?: unknown
  4354. ): Promise<Record<string, DataStrategyResult>> {
  4355. let loadRouteDefinitionsPromises = matches.map((m) =>
  4356. m.route.lazy
  4357. ? loadLazyRouteModule(m.route, mapRouteProperties, manifest)
  4358. : undefined
  4359. );
  4360. let dsMatches = matches.map((match, i) => {
  4361. let loadRoutePromise = loadRouteDefinitionsPromises[i];
  4362. let shouldLoad = matchesToLoad.some((m) => m.route.id === match.route.id);
  4363. // `resolve` encapsulates route.lazy(), executing the loader/action,
  4364. // and mapping return values/thrown errors to a `DataStrategyResult`. Users
  4365. // can pass a callback to take fine-grained control over the execution
  4366. // of the loader/action
  4367. let resolve: DataStrategyMatch["resolve"] = async (handlerOverride) => {
  4368. if (
  4369. handlerOverride &&
  4370. request.method === "GET" &&
  4371. (match.route.lazy || match.route.loader)
  4372. ) {
  4373. shouldLoad = true;
  4374. }
  4375. return shouldLoad
  4376. ? callLoaderOrAction(
  4377. type,
  4378. request,
  4379. match,
  4380. loadRoutePromise,
  4381. handlerOverride,
  4382. requestContext
  4383. )
  4384. : Promise.resolve({ type: ResultType.data, result: undefined });
  4385. };
  4386. return {
  4387. ...match,
  4388. shouldLoad,
  4389. resolve,
  4390. };
  4391. });
  4392. // Send all matches here to allow for a middleware-type implementation.
  4393. // handler will be a no-op for unneeded routes and we filter those results
  4394. // back out below.
  4395. let results = await dataStrategyImpl({
  4396. matches: dsMatches,
  4397. request,
  4398. params: matches[0].params,
  4399. fetcherKey,
  4400. context: requestContext,
  4401. });
  4402. // Wait for all routes to load here but 'swallow the error since we want
  4403. // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -
  4404. // called from `match.resolve()`
  4405. try {
  4406. await Promise.all(loadRouteDefinitionsPromises);
  4407. } catch (e) {
  4408. // No-op
  4409. }
  4410. return results;
  4411. }
  4412. // Default logic for calling a loader/action is the user has no specified a dataStrategy
  4413. async function callLoaderOrAction(
  4414. type: "loader" | "action",
  4415. request: Request,
  4416. match: AgnosticDataRouteMatch,
  4417. loadRoutePromise: Promise<void> | undefined,
  4418. handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
  4419. staticContext?: unknown
  4420. ): Promise<DataStrategyResult> {
  4421. let result: DataStrategyResult;
  4422. let onReject: (() => void) | undefined;
  4423. let runHandler = (
  4424. handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
  4425. ): Promise<DataStrategyResult> => {
  4426. // Setup a promise we can race against so that abort signals short circuit
  4427. let reject: () => void;
  4428. // This will never resolve so safe to type it as Promise<DataStrategyResult> to
  4429. // satisfy the function return value
  4430. let abortPromise = new Promise<DataStrategyResult>((_, r) => (reject = r));
  4431. onReject = () => reject();
  4432. request.signal.addEventListener("abort", onReject);
  4433. let actualHandler = (ctx?: unknown) => {
  4434. if (typeof handler !== "function") {
  4435. return Promise.reject(
  4436. new Error(
  4437. `You cannot call the handler for a route which defines a boolean ` +
  4438. `"${type}" [routeId: ${match.route.id}]`
  4439. )
  4440. );
  4441. }
  4442. return handler(
  4443. {
  4444. request,
  4445. params: match.params,
  4446. context: staticContext,
  4447. },
  4448. ...(ctx !== undefined ? [ctx] : [])
  4449. );
  4450. };
  4451. let handlerPromise: Promise<DataStrategyResult> = (async () => {
  4452. try {
  4453. let val = await (handlerOverride
  4454. ? handlerOverride((ctx: unknown) => actualHandler(ctx))
  4455. : actualHandler());
  4456. return { type: "data", result: val };
  4457. } catch (e) {
  4458. return { type: "error", result: e };
  4459. }
  4460. })();
  4461. return Promise.race([handlerPromise, abortPromise]);
  4462. };
  4463. try {
  4464. let handler = match.route[type];
  4465. // If we have a route.lazy promise, await that first
  4466. if (loadRoutePromise) {
  4467. if (handler) {
  4468. // Run statically defined handler in parallel with lazy()
  4469. let handlerError;
  4470. let [value] = await Promise.all([
  4471. // If the handler throws, don't let it immediately bubble out,
  4472. // since we need to let the lazy() execution finish so we know if this
  4473. // route has a boundary that can handle the error
  4474. runHandler(handler).catch((e) => {
  4475. handlerError = e;
  4476. }),
  4477. loadRoutePromise,
  4478. ]);
  4479. if (handlerError !== undefined) {
  4480. throw handlerError;
  4481. }
  4482. result = value!;
  4483. } else {
  4484. // Load lazy route module, then run any returned handler
  4485. await loadRoutePromise;
  4486. handler = match.route[type];
  4487. if (handler) {
  4488. // Handler still runs even if we got interrupted to maintain consistency
  4489. // with un-abortable behavior of handler execution on non-lazy or
  4490. // previously-lazy-loaded routes
  4491. result = await runHandler(handler);
  4492. } else if (type === "action") {
  4493. let url = new URL(request.url);
  4494. let pathname = url.pathname + url.search;
  4495. throw getInternalRouterError(405, {
  4496. method: request.method,
  4497. pathname,
  4498. routeId: match.route.id,
  4499. });
  4500. } else {
  4501. // lazy() route has no loader to run. Short circuit here so we don't
  4502. // hit the invariant below that errors on returning undefined.
  4503. return { type: ResultType.data, result: undefined };
  4504. }
  4505. }
  4506. } else if (!handler) {
  4507. let url = new URL(request.url);
  4508. let pathname = url.pathname + url.search;
  4509. throw getInternalRouterError(404, {
  4510. pathname,
  4511. });
  4512. } else {
  4513. result = await runHandler(handler);
  4514. }
  4515. invariant(
  4516. result.result !== undefined,
  4517. `You defined ${type === "action" ? "an action" : "a loader"} for route ` +
  4518. `"${match.route.id}" but didn't return anything from your \`${type}\` ` +
  4519. `function. Please return a value or \`null\`.`
  4520. );
  4521. } catch (e) {
  4522. // We should already be catching and converting normal handler executions to
  4523. // DataStrategyResults and returning them, so anything that throws here is an
  4524. // unexpected error we still need to wrap
  4525. return { type: ResultType.error, result: e };
  4526. } finally {
  4527. if (onReject) {
  4528. request.signal.removeEventListener("abort", onReject);
  4529. }
  4530. }
  4531. return result;
  4532. }
  4533. async function convertDataStrategyResultToDataResult(
  4534. dataStrategyResult: DataStrategyResult
  4535. ): Promise<DataResult> {
  4536. let { result, type } = dataStrategyResult;
  4537. if (isResponse(result)) {
  4538. let data: any;
  4539. try {
  4540. let contentType = result.headers.get("Content-Type");
  4541. // Check between word boundaries instead of startsWith() due to the last
  4542. // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
  4543. if (contentType && /\bapplication\/json\b/.test(contentType)) {
  4544. if (result.body == null) {
  4545. data = null;
  4546. } else {
  4547. data = await result.json();
  4548. }
  4549. } else {
  4550. data = await result.text();
  4551. }
  4552. } catch (e) {
  4553. return { type: ResultType.error, error: e };
  4554. }
  4555. if (type === ResultType.error) {
  4556. return {
  4557. type: ResultType.error,
  4558. error: new ErrorResponseImpl(result.status, result.statusText, data),
  4559. statusCode: result.status,
  4560. headers: result.headers,
  4561. };
  4562. }
  4563. return {
  4564. type: ResultType.data,
  4565. data,
  4566. statusCode: result.status,
  4567. headers: result.headers,
  4568. };
  4569. }
  4570. if (type === ResultType.error) {
  4571. if (isDataWithResponseInit(result)) {
  4572. if (result.data instanceof Error) {
  4573. return {
  4574. type: ResultType.error,
  4575. error: result.data,
  4576. statusCode: result.init?.status,
  4577. headers: result.init?.headers
  4578. ? new Headers(result.init.headers)
  4579. : undefined,
  4580. };
  4581. }
  4582. // Convert thrown data() to ErrorResponse instances
  4583. return {
  4584. type: ResultType.error,
  4585. error: new ErrorResponseImpl(
  4586. result.init?.status || 500,
  4587. undefined,
  4588. result.data
  4589. ),
  4590. statusCode: isRouteErrorResponse(result) ? result.status : undefined,
  4591. headers: result.init?.headers
  4592. ? new Headers(result.init.headers)
  4593. : undefined,
  4594. };
  4595. }
  4596. return {
  4597. type: ResultType.error,
  4598. error: result,
  4599. statusCode: isRouteErrorResponse(result) ? result.status : undefined,
  4600. };
  4601. }
  4602. if (isDeferredData(result)) {
  4603. return {
  4604. type: ResultType.deferred,
  4605. deferredData: result,
  4606. statusCode: result.init?.status,
  4607. headers: result.init?.headers && new Headers(result.init.headers),
  4608. };
  4609. }
  4610. if (isDataWithResponseInit(result)) {
  4611. return {
  4612. type: ResultType.data,
  4613. data: result.data,
  4614. statusCode: result.init?.status,
  4615. headers: result.init?.headers
  4616. ? new Headers(result.init.headers)
  4617. : undefined,
  4618. };
  4619. }
  4620. return { type: ResultType.data, data: result };
  4621. }
  4622. // Support relative routing in internal redirects
  4623. function normalizeRelativeRoutingRedirectResponse(
  4624. response: Response,
  4625. request: Request,
  4626. routeId: string,
  4627. matches: AgnosticDataRouteMatch[],
  4628. basename: string,
  4629. v7_relativeSplatPath: boolean
  4630. ) {
  4631. let location = response.headers.get("Location");
  4632. invariant(
  4633. location,
  4634. "Redirects returned/thrown from loaders/actions must have a Location header"
  4635. );
  4636. if (!ABSOLUTE_URL_REGEX.test(location)) {
  4637. let trimmedMatches = matches.slice(
  4638. 0,
  4639. matches.findIndex((m) => m.route.id === routeId) + 1
  4640. );
  4641. location = normalizeTo(
  4642. new URL(request.url),
  4643. trimmedMatches,
  4644. basename,
  4645. true,
  4646. location,
  4647. v7_relativeSplatPath
  4648. );
  4649. response.headers.set("Location", location);
  4650. }
  4651. return response;
  4652. }
  4653. function normalizeRedirectLocation(
  4654. location: string,
  4655. currentUrl: URL,
  4656. basename: string,
  4657. historyInstance: History,
  4658. ): string {
  4659. // Match Chrome's behavior:
  4660. // https://github.com/chromium/chromium/blob/216dbeb61db0c667e62082e5f5400a32d6983df3/content/public/common/url_utils.cc#L82
  4661. let invalidProtocols = [
  4662. "about:",
  4663. "blob:",
  4664. "chrome:",
  4665. "chrome-untrusted:",
  4666. "content:",
  4667. "data:",
  4668. "devtools:",
  4669. "file:",
  4670. "filesystem:",
  4671. // eslint-disable-next-line no-script-url
  4672. "javascript:",
  4673. ];
  4674. if (ABSOLUTE_URL_REGEX.test(location)) {
  4675. // Strip off the protocol+origin for same-origin + same-basename absolute redirects
  4676. let normalizedLocation = location;
  4677. let url = normalizedLocation.startsWith("//")
  4678. ? new URL(currentUrl.protocol + normalizedLocation)
  4679. : new URL(normalizedLocation);
  4680. if (invalidProtocols.includes(url.protocol)) {
  4681. throw new Error("Invalid redirect location");
  4682. }
  4683. let isSameBasename = stripBasename(url.pathname, basename) != null;
  4684. if (url.origin === currentUrl.origin && isSameBasename) {
  4685. return url.pathname + url.search + url.hash;
  4686. }
  4687. }
  4688. try {
  4689. let url = historyInstance.createURL(location);
  4690. if (invalidProtocols.includes(url.protocol)) {
  4691. throw new Error("Invalid redirect location");
  4692. }
  4693. } catch (e) {}
  4694. return location;
  4695. }
  4696. // Utility method for creating the Request instances for loaders/actions during
  4697. // client-side navigations and fetches. During SSR we will always have a
  4698. // Request instance from the static handler (query/queryRoute)
  4699. function createClientSideRequest(
  4700. history: History,
  4701. location: string | Location,
  4702. signal: AbortSignal,
  4703. submission?: Submission
  4704. ): Request {
  4705. let url = history.createURL(stripHashFromPath(location)).toString();
  4706. let init: RequestInit = { signal };
  4707. if (submission && isMutationMethod(submission.formMethod)) {
  4708. let { formMethod, formEncType } = submission;
  4709. // Didn't think we needed this but it turns out unlike other methods, patch
  4710. // won't be properly normalized to uppercase and results in a 405 error.
  4711. // See: https://fetch.spec.whatwg.org/#concept-method
  4712. init.method = formMethod.toUpperCase();
  4713. if (formEncType === "application/json") {
  4714. init.headers = new Headers({ "Content-Type": formEncType });
  4715. init.body = JSON.stringify(submission.json);
  4716. } else if (formEncType === "text/plain") {
  4717. // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
  4718. init.body = submission.text;
  4719. } else if (
  4720. formEncType === "application/x-www-form-urlencoded" &&
  4721. submission.formData
  4722. ) {
  4723. // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
  4724. init.body = convertFormDataToSearchParams(submission.formData);
  4725. } else {
  4726. // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
  4727. init.body = submission.formData;
  4728. }
  4729. }
  4730. return new Request(url, init);
  4731. }
  4732. function convertFormDataToSearchParams(formData: FormData): URLSearchParams {
  4733. let searchParams = new URLSearchParams();
  4734. for (let [key, value] of formData.entries()) {
  4735. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
  4736. searchParams.append(key, typeof value === "string" ? value : value.name);
  4737. }
  4738. return searchParams;
  4739. }
  4740. function convertSearchParamsToFormData(
  4741. searchParams: URLSearchParams
  4742. ): FormData {
  4743. let formData = new FormData();
  4744. for (let [key, value] of searchParams.entries()) {
  4745. formData.append(key, value);
  4746. }
  4747. return formData;
  4748. }
  4749. function processRouteLoaderData(
  4750. matches: AgnosticDataRouteMatch[],
  4751. results: Record<string, DataResult>,
  4752. pendingActionResult: PendingActionResult | undefined,
  4753. activeDeferreds: Map<string, DeferredData>,
  4754. skipLoaderErrorBubbling: boolean
  4755. ): {
  4756. loaderData: RouterState["loaderData"];
  4757. errors: RouterState["errors"] | null;
  4758. statusCode: number;
  4759. loaderHeaders: Record<string, Headers>;
  4760. } {
  4761. // Fill in loaderData/errors from our loaders
  4762. let loaderData: RouterState["loaderData"] = {};
  4763. let errors: RouterState["errors"] | null = null;
  4764. let statusCode: number | undefined;
  4765. let foundError = false;
  4766. let loaderHeaders: Record<string, Headers> = {};
  4767. let pendingError =
  4768. pendingActionResult && isErrorResult(pendingActionResult[1])
  4769. ? pendingActionResult[1].error
  4770. : undefined;
  4771. // Process loader results into state.loaderData/state.errors
  4772. matches.forEach((match) => {
  4773. if (!(match.route.id in results)) {
  4774. return;
  4775. }
  4776. let id = match.route.id;
  4777. let result = results[id];
  4778. invariant(
  4779. !isRedirectResult(result),
  4780. "Cannot handle redirect results in processLoaderData"
  4781. );
  4782. if (isErrorResult(result)) {
  4783. let error = result.error;
  4784. // If we have a pending action error, we report it at the highest-route
  4785. // that throws a loader error, and then clear it out to indicate that
  4786. // it was consumed
  4787. if (pendingError !== undefined) {
  4788. error = pendingError;
  4789. pendingError = undefined;
  4790. }
  4791. errors = errors || {};
  4792. if (skipLoaderErrorBubbling) {
  4793. errors[id] = error;
  4794. } else {
  4795. // Look upwards from the matched route for the closest ancestor error
  4796. // boundary, defaulting to the root match. Prefer higher error values
  4797. // if lower errors bubble to the same boundary
  4798. let boundaryMatch = findNearestBoundary(matches, id);
  4799. if (errors[boundaryMatch.route.id] == null) {
  4800. errors[boundaryMatch.route.id] = error;
  4801. }
  4802. }
  4803. // Clear our any prior loaderData for the throwing route
  4804. loaderData[id] = undefined;
  4805. // Once we find our first (highest) error, we set the status code and
  4806. // prevent deeper status codes from overriding
  4807. if (!foundError) {
  4808. foundError = true;
  4809. statusCode = isRouteErrorResponse(result.error)
  4810. ? result.error.status
  4811. : 500;
  4812. }
  4813. if (result.headers) {
  4814. loaderHeaders[id] = result.headers;
  4815. }
  4816. } else {
  4817. if (isDeferredResult(result)) {
  4818. activeDeferreds.set(id, result.deferredData);
  4819. loaderData[id] = result.deferredData.data;
  4820. // Error status codes always override success status codes, but if all
  4821. // loaders are successful we take the deepest status code.
  4822. if (
  4823. result.statusCode != null &&
  4824. result.statusCode !== 200 &&
  4825. !foundError
  4826. ) {
  4827. statusCode = result.statusCode;
  4828. }
  4829. if (result.headers) {
  4830. loaderHeaders[id] = result.headers;
  4831. }
  4832. } else {
  4833. loaderData[id] = result.data;
  4834. // Error status codes always override success status codes, but if all
  4835. // loaders are successful we take the deepest status code.
  4836. if (result.statusCode && result.statusCode !== 200 && !foundError) {
  4837. statusCode = result.statusCode;
  4838. }
  4839. if (result.headers) {
  4840. loaderHeaders[id] = result.headers;
  4841. }
  4842. }
  4843. }
  4844. });
  4845. // If we didn't consume the pending action error (i.e., all loaders
  4846. // resolved), then consume it here. Also clear out any loaderData for the
  4847. // throwing route
  4848. if (pendingError !== undefined && pendingActionResult) {
  4849. errors = { [pendingActionResult[0]]: pendingError };
  4850. loaderData[pendingActionResult[0]] = undefined;
  4851. }
  4852. return {
  4853. loaderData,
  4854. errors,
  4855. statusCode: statusCode || 200,
  4856. loaderHeaders,
  4857. };
  4858. }
  4859. function processLoaderData(
  4860. state: RouterState,
  4861. matches: AgnosticDataRouteMatch[],
  4862. results: Record<string, DataResult>,
  4863. pendingActionResult: PendingActionResult | undefined,
  4864. revalidatingFetchers: RevalidatingFetcher[],
  4865. fetcherResults: Record<string, DataResult>,
  4866. activeDeferreds: Map<string, DeferredData>
  4867. ): {
  4868. loaderData: RouterState["loaderData"];
  4869. errors?: RouterState["errors"];
  4870. } {
  4871. let { loaderData, errors } = processRouteLoaderData(
  4872. matches,
  4873. results,
  4874. pendingActionResult,
  4875. activeDeferreds,
  4876. false // This method is only called client side so we always want to bubble
  4877. );
  4878. // Process results from our revalidating fetchers
  4879. revalidatingFetchers.forEach((rf) => {
  4880. let { key, match, controller } = rf;
  4881. let result = fetcherResults[key];
  4882. invariant(result, "Did not find corresponding fetcher result");
  4883. // Process fetcher non-redirect errors
  4884. if (controller && controller.signal.aborted) {
  4885. // Nothing to do for aborted fetchers
  4886. return;
  4887. } else if (isErrorResult(result)) {
  4888. let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
  4889. if (!(errors && errors[boundaryMatch.route.id])) {
  4890. errors = {
  4891. ...errors,
  4892. [boundaryMatch.route.id]: result.error,
  4893. };
  4894. }
  4895. state.fetchers.delete(key);
  4896. } else if (isRedirectResult(result)) {
  4897. // Should never get here, redirects should get processed above, but we
  4898. // keep this to type narrow to a success result in the else
  4899. invariant(false, "Unhandled fetcher revalidation redirect");
  4900. } else if (isDeferredResult(result)) {
  4901. // Should never get here, deferred data should be awaited for fetchers
  4902. // in resolveDeferredResults
  4903. invariant(false, "Unhandled fetcher deferred data");
  4904. } else {
  4905. let doneFetcher = getDoneFetcher(result.data);
  4906. state.fetchers.set(key, doneFetcher);
  4907. }
  4908. });
  4909. return { loaderData, errors };
  4910. }
  4911. function mergeLoaderData(
  4912. loaderData: RouteData,
  4913. newLoaderData: RouteData,
  4914. matches: AgnosticDataRouteMatch[],
  4915. errors: RouteData | null | undefined
  4916. ): RouteData {
  4917. let mergedLoaderData = { ...newLoaderData };
  4918. for (let match of matches) {
  4919. let id = match.route.id;
  4920. if (newLoaderData.hasOwnProperty(id)) {
  4921. if (newLoaderData[id] !== undefined) {
  4922. mergedLoaderData[id] = newLoaderData[id];
  4923. } else {
  4924. // No-op - this is so we ignore existing data if we have a key in the
  4925. // incoming object with an undefined value, which is how we unset a prior
  4926. // loaderData if we encounter a loader error
  4927. }
  4928. } else if (loaderData[id] !== undefined && match.route.loader) {
  4929. // Preserve existing keys not included in newLoaderData and where a loader
  4930. // wasn't removed by HMR
  4931. mergedLoaderData[id] = loaderData[id];
  4932. }
  4933. if (errors && errors.hasOwnProperty(id)) {
  4934. // Don't keep any loader data below the boundary
  4935. break;
  4936. }
  4937. }
  4938. return mergedLoaderData;
  4939. }
  4940. function getActionDataForCommit(
  4941. pendingActionResult: PendingActionResult | undefined
  4942. ) {
  4943. if (!pendingActionResult) {
  4944. return {};
  4945. }
  4946. return isErrorResult(pendingActionResult[1])
  4947. ? {
  4948. // Clear out prior actionData on errors
  4949. actionData: {},
  4950. }
  4951. : {
  4952. actionData: {
  4953. [pendingActionResult[0]]: pendingActionResult[1].data,
  4954. },
  4955. };
  4956. }
  4957. // Find the nearest error boundary, looking upwards from the leaf route (or the
  4958. // route specified by routeId) for the closest ancestor error boundary,
  4959. // defaulting to the root match
  4960. function findNearestBoundary(
  4961. matches: AgnosticDataRouteMatch[],
  4962. routeId?: string
  4963. ): AgnosticDataRouteMatch {
  4964. let eligibleMatches = routeId
  4965. ? matches.slice(0, matches.findIndex((m) => m.route.id === routeId) + 1)
  4966. : [...matches];
  4967. return (
  4968. eligibleMatches.reverse().find((m) => m.route.hasErrorBoundary === true) ||
  4969. matches[0]
  4970. );
  4971. }
  4972. function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
  4973. matches: AgnosticDataRouteMatch[];
  4974. route: AgnosticDataRouteObject;
  4975. } {
  4976. // Prefer a root layout route if present, otherwise shim in a route object
  4977. let route =
  4978. routes.length === 1
  4979. ? routes[0]
  4980. : routes.find((r) => r.index || !r.path || r.path === "/") || {
  4981. id: `__shim-error-route__`,
  4982. };
  4983. return {
  4984. matches: [
  4985. {
  4986. params: {},
  4987. pathname: "",
  4988. pathnameBase: "",
  4989. route,
  4990. },
  4991. ],
  4992. route,
  4993. };
  4994. }
  4995. function getInternalRouterError(
  4996. status: number,
  4997. {
  4998. pathname,
  4999. routeId,
  5000. method,
  5001. type,
  5002. message,
  5003. }: {
  5004. pathname?: string;
  5005. routeId?: string;
  5006. method?: string;
  5007. type?: "defer-action" | "invalid-body";
  5008. message?: string;
  5009. } = {}
  5010. ) {
  5011. let statusText = "Unknown Server Error";
  5012. let errorMessage = "Unknown @remix-run/router error";
  5013. if (status === 400) {
  5014. statusText = "Bad Request";
  5015. if (method && pathname && routeId) {
  5016. errorMessage =
  5017. `You made a ${method} request to "${pathname}" but ` +
  5018. `did not provide a \`loader\` for route "${routeId}", ` +
  5019. `so there is no way to handle the request.`;
  5020. } else if (type === "defer-action") {
  5021. errorMessage = "defer() is not supported in actions";
  5022. } else if (type === "invalid-body") {
  5023. errorMessage = "Unable to encode submission body";
  5024. }
  5025. } else if (status === 403) {
  5026. statusText = "Forbidden";
  5027. errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
  5028. } else if (status === 404) {
  5029. statusText = "Not Found";
  5030. errorMessage = `No route matches URL "${pathname}"`;
  5031. } else if (status === 405) {
  5032. statusText = "Method Not Allowed";
  5033. if (method && pathname && routeId) {
  5034. errorMessage =
  5035. `You made a ${method.toUpperCase()} request to "${pathname}" but ` +
  5036. `did not provide an \`action\` for route "${routeId}", ` +
  5037. `so there is no way to handle the request.`;
  5038. } else if (method) {
  5039. errorMessage = `Invalid request method "${method.toUpperCase()}"`;
  5040. }
  5041. }
  5042. return new ErrorResponseImpl(
  5043. status || 500,
  5044. statusText,
  5045. new Error(errorMessage),
  5046. true
  5047. );
  5048. }
  5049. // Find any returned redirect errors, starting from the lowest match
  5050. function findRedirect(
  5051. results: Record<string, DataResult>
  5052. ): { key: string; result: RedirectResult } | undefined {
  5053. let entries = Object.entries(results);
  5054. for (let i = entries.length - 1; i >= 0; i--) {
  5055. let [key, result] = entries[i];
  5056. if (isRedirectResult(result)) {
  5057. return { key, result };
  5058. }
  5059. }
  5060. }
  5061. function stripHashFromPath(path: To) {
  5062. let parsedPath = typeof path === "string" ? parsePath(path) : path;
  5063. return createPath({ ...parsedPath, hash: "" });
  5064. }
  5065. function isHashChangeOnly(a: Location, b: Location): boolean {
  5066. if (a.pathname !== b.pathname || a.search !== b.search) {
  5067. return false;
  5068. }
  5069. if (a.hash === "") {
  5070. // /page -> /page#hash
  5071. return b.hash !== "";
  5072. } else if (a.hash === b.hash) {
  5073. // /page#hash -> /page#hash
  5074. return true;
  5075. } else if (b.hash !== "") {
  5076. // /page#hash -> /page#other
  5077. return true;
  5078. }
  5079. // If the hash is removed the browser will re-perform a request to the server
  5080. // /page#hash -> /page
  5081. return false;
  5082. }
  5083. function isPromise<T = unknown>(val: unknown): val is Promise<T> {
  5084. return typeof val === "object" && val != null && "then" in val;
  5085. }
  5086. function isDataStrategyResult(result: unknown): result is DataStrategyResult {
  5087. return (
  5088. result != null &&
  5089. typeof result === "object" &&
  5090. "type" in result &&
  5091. "result" in result &&
  5092. (result.type === ResultType.data || result.type === ResultType.error)
  5093. );
  5094. }
  5095. function isRedirectDataStrategyResultResult(result: DataStrategyResult) {
  5096. return (
  5097. isResponse(result.result) && redirectStatusCodes.has(result.result.status)
  5098. );
  5099. }
  5100. function isDeferredResult(result: DataResult): result is DeferredResult {
  5101. return result.type === ResultType.deferred;
  5102. }
  5103. function isErrorResult(result: DataResult): result is ErrorResult {
  5104. return result.type === ResultType.error;
  5105. }
  5106. function isRedirectResult(result?: DataResult): result is RedirectResult {
  5107. return (result && result.type) === ResultType.redirect;
  5108. }
  5109. export function isDataWithResponseInit(
  5110. value: any
  5111. ): value is DataWithResponseInit<unknown> {
  5112. return (
  5113. typeof value === "object" &&
  5114. value != null &&
  5115. "type" in value &&
  5116. "data" in value &&
  5117. "init" in value &&
  5118. value.type === "DataWithResponseInit"
  5119. );
  5120. }
  5121. export function isDeferredData(value: any): value is DeferredData {
  5122. let deferred: DeferredData = value;
  5123. return (
  5124. deferred &&
  5125. typeof deferred === "object" &&
  5126. typeof deferred.data === "object" &&
  5127. typeof deferred.subscribe === "function" &&
  5128. typeof deferred.cancel === "function" &&
  5129. typeof deferred.resolveData === "function"
  5130. );
  5131. }
  5132. function isResponse(value: any): value is Response {
  5133. return (
  5134. value != null &&
  5135. typeof value.status === "number" &&
  5136. typeof value.statusText === "string" &&
  5137. typeof value.headers === "object" &&
  5138. typeof value.body !== "undefined"
  5139. );
  5140. }
  5141. function isRedirectResponse(result: any): result is Response {
  5142. if (!isResponse(result)) {
  5143. return false;
  5144. }
  5145. let status = result.status;
  5146. let location = result.headers.get("Location");
  5147. return status >= 300 && status <= 399 && location != null;
  5148. }
  5149. function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
  5150. return validRequestMethods.has(method.toLowerCase() as FormMethod);
  5151. }
  5152. function isMutationMethod(
  5153. method: string
  5154. ): method is MutationFormMethod | V7_MutationFormMethod {
  5155. return validMutationMethods.has(method.toLowerCase() as MutationFormMethod);
  5156. }
  5157. async function resolveNavigationDeferredResults(
  5158. matches: (AgnosticDataRouteMatch | null)[],
  5159. results: Record<string, DataResult>,
  5160. signal: AbortSignal,
  5161. currentMatches: AgnosticDataRouteMatch[],
  5162. currentLoaderData: RouteData
  5163. ) {
  5164. let entries = Object.entries(results);
  5165. for (let index = 0; index < entries.length; index++) {
  5166. let [routeId, result] = entries[index];
  5167. let match = matches.find((m) => m?.route.id === routeId);
  5168. // If we don't have a match, then we can have a deferred result to do
  5169. // anything with. This is for revalidating fetchers where the route was
  5170. // removed during HMR
  5171. if (!match) {
  5172. continue;
  5173. }
  5174. let currentMatch = currentMatches.find(
  5175. (m) => m.route.id === match!.route.id
  5176. );
  5177. let isRevalidatingLoader =
  5178. currentMatch != null &&
  5179. !isNewRouteInstance(currentMatch, match) &&
  5180. (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
  5181. if (isDeferredResult(result) && isRevalidatingLoader) {
  5182. // Note: we do not have to touch activeDeferreds here since we race them
  5183. // against the signal in resolveDeferredData and they'll get aborted
  5184. // there if needed
  5185. await resolveDeferredData(result, signal, false).then((result) => {
  5186. if (result) {
  5187. results[routeId] = result;
  5188. }
  5189. });
  5190. }
  5191. }
  5192. }
  5193. async function resolveFetcherDeferredResults(
  5194. matches: (AgnosticDataRouteMatch | null)[],
  5195. results: Record<string, DataResult>,
  5196. revalidatingFetchers: RevalidatingFetcher[]
  5197. ) {
  5198. for (let index = 0; index < revalidatingFetchers.length; index++) {
  5199. let { key, routeId, controller } = revalidatingFetchers[index];
  5200. let result = results[key];
  5201. let match = matches.find((m) => m?.route.id === routeId);
  5202. // If we don't have a match, then we can have a deferred result to do
  5203. // anything with. This is for revalidating fetchers where the route was
  5204. // removed during HMR
  5205. if (!match) {
  5206. continue;
  5207. }
  5208. if (isDeferredResult(result)) {
  5209. // Note: we do not have to touch activeDeferreds here since we race them
  5210. // against the signal in resolveDeferredData and they'll get aborted
  5211. // there if needed
  5212. invariant(
  5213. controller,
  5214. "Expected an AbortController for revalidating fetcher deferred result"
  5215. );
  5216. await resolveDeferredData(result, controller.signal, true).then(
  5217. (result) => {
  5218. if (result) {
  5219. results[key] = result;
  5220. }
  5221. }
  5222. );
  5223. }
  5224. }
  5225. }
  5226. async function resolveDeferredData(
  5227. result: DeferredResult,
  5228. signal: AbortSignal,
  5229. unwrap = false
  5230. ): Promise<SuccessResult | ErrorResult | undefined> {
  5231. let aborted = await result.deferredData.resolveData(signal);
  5232. if (aborted) {
  5233. return;
  5234. }
  5235. if (unwrap) {
  5236. try {
  5237. return {
  5238. type: ResultType.data,
  5239. data: result.deferredData.unwrappedData,
  5240. };
  5241. } catch (e) {
  5242. // Handle any TrackedPromise._error values encountered while unwrapping
  5243. return {
  5244. type: ResultType.error,
  5245. error: e,
  5246. };
  5247. }
  5248. }
  5249. return {
  5250. type: ResultType.data,
  5251. data: result.deferredData.data,
  5252. };
  5253. }
  5254. function hasNakedIndexQuery(search: string): boolean {
  5255. return new URLSearchParams(search).getAll("index").some((v) => v === "");
  5256. }
  5257. function getTargetMatch(
  5258. matches: AgnosticDataRouteMatch[],
  5259. location: Location | string
  5260. ) {
  5261. let search =
  5262. typeof location === "string" ? parsePath(location).search : location.search;
  5263. if (
  5264. matches[matches.length - 1].route.index &&
  5265. hasNakedIndexQuery(search || "")
  5266. ) {
  5267. // Return the leaf index route when index is present
  5268. return matches[matches.length - 1];
  5269. }
  5270. // Otherwise grab the deepest "path contributing" match (ignoring index and
  5271. // pathless layout routes)
  5272. let pathMatches = getPathContributingMatches(matches);
  5273. return pathMatches[pathMatches.length - 1];
  5274. }
  5275. function getSubmissionFromNavigation(
  5276. navigation: Navigation
  5277. ): Submission | undefined {
  5278. let { formMethod, formAction, formEncType, text, formData, json } =
  5279. navigation;
  5280. if (!formMethod || !formAction || !formEncType) {
  5281. return;
  5282. }
  5283. if (text != null) {
  5284. return {
  5285. formMethod,
  5286. formAction,
  5287. formEncType,
  5288. formData: undefined,
  5289. json: undefined,
  5290. text,
  5291. };
  5292. } else if (formData != null) {
  5293. return {
  5294. formMethod,
  5295. formAction,
  5296. formEncType,
  5297. formData,
  5298. json: undefined,
  5299. text: undefined,
  5300. };
  5301. } else if (json !== undefined) {
  5302. return {
  5303. formMethod,
  5304. formAction,
  5305. formEncType,
  5306. formData: undefined,
  5307. json,
  5308. text: undefined,
  5309. };
  5310. }
  5311. }
  5312. function getLoadingNavigation(
  5313. location: Location,
  5314. submission?: Submission
  5315. ): NavigationStates["Loading"] {
  5316. if (submission) {
  5317. let navigation: NavigationStates["Loading"] = {
  5318. state: "loading",
  5319. location,
  5320. formMethod: submission.formMethod,
  5321. formAction: submission.formAction,
  5322. formEncType: submission.formEncType,
  5323. formData: submission.formData,
  5324. json: submission.json,
  5325. text: submission.text,
  5326. };
  5327. return navigation;
  5328. } else {
  5329. let navigation: NavigationStates["Loading"] = {
  5330. state: "loading",
  5331. location,
  5332. formMethod: undefined,
  5333. formAction: undefined,
  5334. formEncType: undefined,
  5335. formData: undefined,
  5336. json: undefined,
  5337. text: undefined,
  5338. };
  5339. return navigation;
  5340. }
  5341. }
  5342. function getSubmittingNavigation(
  5343. location: Location,
  5344. submission: Submission
  5345. ): NavigationStates["Submitting"] {
  5346. let navigation: NavigationStates["Submitting"] = {
  5347. state: "submitting",
  5348. location,
  5349. formMethod: submission.formMethod,
  5350. formAction: submission.formAction,
  5351. formEncType: submission.formEncType,
  5352. formData: submission.formData,
  5353. json: submission.json,
  5354. text: submission.text,
  5355. };
  5356. return navigation;
  5357. }
  5358. function getLoadingFetcher(
  5359. submission?: Submission,
  5360. data?: Fetcher["data"]
  5361. ): FetcherStates["Loading"] {
  5362. if (submission) {
  5363. let fetcher: FetcherStates["Loading"] = {
  5364. state: "loading",
  5365. formMethod: submission.formMethod,
  5366. formAction: submission.formAction,
  5367. formEncType: submission.formEncType,
  5368. formData: submission.formData,
  5369. json: submission.json,
  5370. text: submission.text,
  5371. data,
  5372. };
  5373. return fetcher;
  5374. } else {
  5375. let fetcher: FetcherStates["Loading"] = {
  5376. state: "loading",
  5377. formMethod: undefined,
  5378. formAction: undefined,
  5379. formEncType: undefined,
  5380. formData: undefined,
  5381. json: undefined,
  5382. text: undefined,
  5383. data,
  5384. };
  5385. return fetcher;
  5386. }
  5387. }
  5388. function getSubmittingFetcher(
  5389. submission: Submission,
  5390. existingFetcher?: Fetcher
  5391. ): FetcherStates["Submitting"] {
  5392. let fetcher: FetcherStates["Submitting"] = {
  5393. state: "submitting",
  5394. formMethod: submission.formMethod,
  5395. formAction: submission.formAction,
  5396. formEncType: submission.formEncType,
  5397. formData: submission.formData,
  5398. json: submission.json,
  5399. text: submission.text,
  5400. data: existingFetcher ? existingFetcher.data : undefined,
  5401. };
  5402. return fetcher;
  5403. }
  5404. function getDoneFetcher(data: Fetcher["data"]): FetcherStates["Idle"] {
  5405. let fetcher: FetcherStates["Idle"] = {
  5406. state: "idle",
  5407. formMethod: undefined,
  5408. formAction: undefined,
  5409. formEncType: undefined,
  5410. formData: undefined,
  5411. json: undefined,
  5412. text: undefined,
  5413. data,
  5414. };
  5415. return fetcher;
  5416. }
  5417. function restoreAppliedTransitions(
  5418. _window: Window,
  5419. transitions: Map<string, Set<string>>
  5420. ) {
  5421. try {
  5422. let sessionPositions = _window.sessionStorage.getItem(
  5423. TRANSITIONS_STORAGE_KEY
  5424. );
  5425. if (sessionPositions) {
  5426. let json = JSON.parse(sessionPositions);
  5427. for (let [k, v] of Object.entries(json || {})) {
  5428. if (v && Array.isArray(v)) {
  5429. transitions.set(k, new Set(v || []));
  5430. }
  5431. }
  5432. }
  5433. } catch (e) {
  5434. // no-op, use default empty object
  5435. }
  5436. }
  5437. function persistAppliedTransitions(
  5438. _window: Window,
  5439. transitions: Map<string, Set<string>>
  5440. ) {
  5441. if (transitions.size > 0) {
  5442. let json: Record<string, string[]> = {};
  5443. for (let [k, v] of transitions) {
  5444. json[k] = [...v];
  5445. }
  5446. try {
  5447. _window.sessionStorage.setItem(
  5448. TRANSITIONS_STORAGE_KEY,
  5449. JSON.stringify(json)
  5450. );
  5451. } catch (error) {
  5452. warning(
  5453. false,
  5454. `Failed to save applied view transitions in sessionStorage (${error}).`
  5455. );
  5456. }
  5457. }
  5458. }
  5459. //#endregion