router.ts 180 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001
  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. );
  1603. replace = location === state.location.pathname + state.location.search;
  1604. }
  1605. await startRedirectNavigation(request, result, true, {
  1606. submission,
  1607. replace,
  1608. });
  1609. return { shortCircuited: true };
  1610. }
  1611. if (isDeferredResult(result)) {
  1612. throw getInternalRouterError(400, { type: "defer-action" });
  1613. }
  1614. if (isErrorResult(result)) {
  1615. // Store off the pending error - we use it to determine which loaders
  1616. // to call and will commit it when we complete the navigation
  1617. let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
  1618. // By default, all submissions to the current location are REPLACE
  1619. // navigations, but if the action threw an error that'll be rendered in
  1620. // an errorElement, we fall back to PUSH so that the user can use the
  1621. // back button to get back to the pre-submission form location to try
  1622. // again
  1623. if ((opts && opts.replace) !== true) {
  1624. pendingAction = HistoryAction.Push;
  1625. }
  1626. return {
  1627. matches,
  1628. pendingActionResult: [boundaryMatch.route.id, result],
  1629. };
  1630. }
  1631. return {
  1632. matches,
  1633. pendingActionResult: [actionMatch.route.id, result],
  1634. };
  1635. }
  1636. // Call all applicable loaders for the given matches, handling redirects,
  1637. // errors, etc.
  1638. async function handleLoaders(
  1639. request: Request,
  1640. location: Location,
  1641. matches: AgnosticDataRouteMatch[],
  1642. isFogOfWar: boolean,
  1643. overrideNavigation?: Navigation,
  1644. submission?: Submission,
  1645. fetcherSubmission?: Submission,
  1646. replace?: boolean,
  1647. initialHydration?: boolean,
  1648. flushSync?: boolean,
  1649. pendingActionResult?: PendingActionResult
  1650. ): Promise<HandleLoadersResult> {
  1651. // Figure out the right navigation we want to use for data loading
  1652. let loadingNavigation =
  1653. overrideNavigation || getLoadingNavigation(location, submission);
  1654. // If this was a redirect from an action we don't have a "submission" but
  1655. // we have it on the loading navigation so use that if available
  1656. let activeSubmission =
  1657. submission ||
  1658. fetcherSubmission ||
  1659. getSubmissionFromNavigation(loadingNavigation);
  1660. // If this is an uninterrupted revalidation, we remain in our current idle
  1661. // state. If not, we need to switch to our loading state and load data,
  1662. // preserving any new action data or existing action data (in the case of
  1663. // a revalidation interrupting an actionReload)
  1664. // If we have partialHydration enabled, then don't update the state for the
  1665. // initial data load since it's not a "navigation"
  1666. let shouldUpdateNavigationState =
  1667. !isUninterruptedRevalidation &&
  1668. (!future.v7_partialHydration || !initialHydration);
  1669. // When fog of war is enabled, we enter our `loading` state earlier so we
  1670. // can discover new routes during the `loading` state. We skip this if
  1671. // we've already run actions since we would have done our matching already.
  1672. // If the children() function threw then, we want to proceed with the
  1673. // partial matches it discovered.
  1674. if (isFogOfWar) {
  1675. if (shouldUpdateNavigationState) {
  1676. let actionData = getUpdatedActionData(pendingActionResult);
  1677. updateState(
  1678. {
  1679. navigation: loadingNavigation,
  1680. ...(actionData !== undefined ? { actionData } : {}),
  1681. },
  1682. {
  1683. flushSync,
  1684. }
  1685. );
  1686. }
  1687. let discoverResult = await discoverRoutes(
  1688. matches,
  1689. location.pathname,
  1690. request.signal
  1691. );
  1692. if (discoverResult.type === "aborted") {
  1693. return { shortCircuited: true };
  1694. } else if (discoverResult.type === "error") {
  1695. let boundaryId = findNearestBoundary(discoverResult.partialMatches)
  1696. .route.id;
  1697. return {
  1698. matches: discoverResult.partialMatches,
  1699. loaderData: {},
  1700. errors: {
  1701. [boundaryId]: discoverResult.error,
  1702. },
  1703. };
  1704. } else if (!discoverResult.matches) {
  1705. let { error, notFoundMatches, route } = handleNavigational404(
  1706. location.pathname
  1707. );
  1708. return {
  1709. matches: notFoundMatches,
  1710. loaderData: {},
  1711. errors: {
  1712. [route.id]: error,
  1713. },
  1714. };
  1715. } else {
  1716. matches = discoverResult.matches;
  1717. }
  1718. }
  1719. let routesToUse = inFlightDataRoutes || dataRoutes;
  1720. let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
  1721. init.history,
  1722. state,
  1723. matches,
  1724. activeSubmission,
  1725. location,
  1726. future.v7_partialHydration && initialHydration === true,
  1727. future.v7_skipActionErrorRevalidation,
  1728. isRevalidationRequired,
  1729. cancelledDeferredRoutes,
  1730. cancelledFetcherLoads,
  1731. deletedFetchers,
  1732. fetchLoadMatches,
  1733. fetchRedirectIds,
  1734. routesToUse,
  1735. basename,
  1736. pendingActionResult
  1737. );
  1738. // Cancel pending deferreds for no-longer-matched routes or routes we're
  1739. // about to reload. Note that if this is an action reload we would have
  1740. // already cancelled all pending deferreds so this would be a no-op
  1741. cancelActiveDeferreds(
  1742. (routeId) =>
  1743. !(matches && matches.some((m) => m.route.id === routeId)) ||
  1744. (matchesToLoad && matchesToLoad.some((m) => m.route.id === routeId))
  1745. );
  1746. pendingNavigationLoadId = ++incrementingLoadId;
  1747. // Short circuit if we have no loaders to run
  1748. if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
  1749. let updatedFetchers = markFetchRedirectsDone();
  1750. completeNavigation(
  1751. location,
  1752. {
  1753. matches,
  1754. loaderData: {},
  1755. // Commit pending error if we're short circuiting
  1756. errors:
  1757. pendingActionResult && isErrorResult(pendingActionResult[1])
  1758. ? { [pendingActionResult[0]]: pendingActionResult[1].error }
  1759. : null,
  1760. ...getActionDataForCommit(pendingActionResult),
  1761. ...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
  1762. },
  1763. { flushSync }
  1764. );
  1765. return { shortCircuited: true };
  1766. }
  1767. if (shouldUpdateNavigationState) {
  1768. let updates: Partial<RouterState> = {};
  1769. if (!isFogOfWar) {
  1770. // Only update navigation/actionNData if we didn't already do it above
  1771. updates.navigation = loadingNavigation;
  1772. let actionData = getUpdatedActionData(pendingActionResult);
  1773. if (actionData !== undefined) {
  1774. updates.actionData = actionData;
  1775. }
  1776. }
  1777. if (revalidatingFetchers.length > 0) {
  1778. updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers);
  1779. }
  1780. updateState(updates, { flushSync });
  1781. }
  1782. revalidatingFetchers.forEach((rf) => {
  1783. abortFetcher(rf.key);
  1784. if (rf.controller) {
  1785. // Fetchers use an independent AbortController so that aborting a fetcher
  1786. // (via deleteFetcher) does not abort the triggering navigation that
  1787. // triggered the revalidation
  1788. fetchControllers.set(rf.key, rf.controller);
  1789. }
  1790. });
  1791. // Proxy navigation abort through to revalidation fetchers
  1792. let abortPendingFetchRevalidations = () =>
  1793. revalidatingFetchers.forEach((f) => abortFetcher(f.key));
  1794. if (pendingNavigationController) {
  1795. pendingNavigationController.signal.addEventListener(
  1796. "abort",
  1797. abortPendingFetchRevalidations
  1798. );
  1799. }
  1800. let { loaderResults, fetcherResults } =
  1801. await callLoadersAndMaybeResolveData(
  1802. state,
  1803. matches,
  1804. matchesToLoad,
  1805. revalidatingFetchers,
  1806. request
  1807. );
  1808. if (request.signal.aborted) {
  1809. return { shortCircuited: true };
  1810. }
  1811. // Clean up _after_ loaders have completed. Don't clean up if we short
  1812. // circuited because fetchControllers would have been aborted and
  1813. // reassigned to new controllers for the next navigation
  1814. if (pendingNavigationController) {
  1815. pendingNavigationController.signal.removeEventListener(
  1816. "abort",
  1817. abortPendingFetchRevalidations
  1818. );
  1819. }
  1820. revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
  1821. // If any loaders returned a redirect Response, start a new REPLACE navigation
  1822. let redirect = findRedirect(loaderResults);
  1823. if (redirect) {
  1824. await startRedirectNavigation(request, redirect.result, true, {
  1825. replace,
  1826. });
  1827. return { shortCircuited: true };
  1828. }
  1829. redirect = findRedirect(fetcherResults);
  1830. if (redirect) {
  1831. // If this redirect came from a fetcher make sure we mark it in
  1832. // fetchRedirectIds so it doesn't get revalidated on the next set of
  1833. // loader executions
  1834. fetchRedirectIds.add(redirect.key);
  1835. await startRedirectNavigation(request, redirect.result, true, {
  1836. replace,
  1837. });
  1838. return { shortCircuited: true };
  1839. }
  1840. // Process and commit output from loaders
  1841. let { loaderData, errors } = processLoaderData(
  1842. state,
  1843. matches,
  1844. loaderResults,
  1845. pendingActionResult,
  1846. revalidatingFetchers,
  1847. fetcherResults,
  1848. activeDeferreds
  1849. );
  1850. // Wire up subscribers to update loaderData as promises settle
  1851. activeDeferreds.forEach((deferredData, routeId) => {
  1852. deferredData.subscribe((aborted) => {
  1853. // Note: No need to updateState here since the TrackedPromise on
  1854. // loaderData is stable across resolve/reject
  1855. // Remove this instance if we were aborted or if promises have settled
  1856. if (aborted || deferredData.done) {
  1857. activeDeferreds.delete(routeId);
  1858. }
  1859. });
  1860. });
  1861. // Preserve SSR errors during partial hydration
  1862. if (future.v7_partialHydration && initialHydration && state.errors) {
  1863. errors = { ...state.errors, ...errors };
  1864. }
  1865. let updatedFetchers = markFetchRedirectsDone();
  1866. let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
  1867. let shouldUpdateFetchers =
  1868. updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
  1869. return {
  1870. matches,
  1871. loaderData,
  1872. errors,
  1873. ...(shouldUpdateFetchers ? { fetchers: new Map(state.fetchers) } : {}),
  1874. };
  1875. }
  1876. function getUpdatedActionData(
  1877. pendingActionResult: PendingActionResult | undefined
  1878. ): Record<string, RouteData> | null | undefined {
  1879. if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
  1880. // This is cast to `any` currently because `RouteData`uses any and it
  1881. // would be a breaking change to use any.
  1882. // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
  1883. return {
  1884. [pendingActionResult[0]]: pendingActionResult[1].data as any,
  1885. };
  1886. } else if (state.actionData) {
  1887. if (Object.keys(state.actionData).length === 0) {
  1888. return null;
  1889. } else {
  1890. return state.actionData;
  1891. }
  1892. }
  1893. }
  1894. function getUpdatedRevalidatingFetchers(
  1895. revalidatingFetchers: RevalidatingFetcher[]
  1896. ) {
  1897. revalidatingFetchers.forEach((rf) => {
  1898. let fetcher = state.fetchers.get(rf.key);
  1899. let revalidatingFetcher = getLoadingFetcher(
  1900. undefined,
  1901. fetcher ? fetcher.data : undefined
  1902. );
  1903. state.fetchers.set(rf.key, revalidatingFetcher);
  1904. });
  1905. return new Map(state.fetchers);
  1906. }
  1907. // Trigger a fetcher load/submit for the given fetcher key
  1908. function fetch(
  1909. key: string,
  1910. routeId: string,
  1911. href: string | null,
  1912. opts?: RouterFetchOptions
  1913. ) {
  1914. if (isServer) {
  1915. throw new Error(
  1916. "router.fetch() was called during the server render, but it shouldn't be. " +
  1917. "You are likely calling a useFetcher() method in the body of your component. " +
  1918. "Try moving it to a useEffect or a callback."
  1919. );
  1920. }
  1921. abortFetcher(key);
  1922. let flushSync = (opts && opts.flushSync) === true;
  1923. let routesToUse = inFlightDataRoutes || dataRoutes;
  1924. let normalizedPath = normalizeTo(
  1925. state.location,
  1926. state.matches,
  1927. basename,
  1928. future.v7_prependBasename,
  1929. href,
  1930. future.v7_relativeSplatPath,
  1931. routeId,
  1932. opts?.relative
  1933. );
  1934. let matches = matchRoutes(routesToUse, normalizedPath, basename);
  1935. let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath);
  1936. if (fogOfWar.active && fogOfWar.matches) {
  1937. matches = fogOfWar.matches;
  1938. }
  1939. if (!matches) {
  1940. setFetcherError(
  1941. key,
  1942. routeId,
  1943. getInternalRouterError(404, { pathname: normalizedPath }),
  1944. { flushSync }
  1945. );
  1946. return;
  1947. }
  1948. let { path, submission, error } = normalizeNavigateOptions(
  1949. future.v7_normalizeFormMethod,
  1950. true,
  1951. normalizedPath,
  1952. opts
  1953. );
  1954. if (error) {
  1955. setFetcherError(key, routeId, error, { flushSync });
  1956. return;
  1957. }
  1958. let match = getTargetMatch(matches, path);
  1959. let preventScrollReset = (opts && opts.preventScrollReset) === true;
  1960. if (submission && isMutationMethod(submission.formMethod)) {
  1961. handleFetcherAction(
  1962. key,
  1963. routeId,
  1964. path,
  1965. match,
  1966. matches,
  1967. fogOfWar.active,
  1968. flushSync,
  1969. preventScrollReset,
  1970. submission
  1971. );
  1972. return;
  1973. }
  1974. // Store off the match so we can call it's shouldRevalidate on subsequent
  1975. // revalidations
  1976. fetchLoadMatches.set(key, { routeId, path });
  1977. handleFetcherLoader(
  1978. key,
  1979. routeId,
  1980. path,
  1981. match,
  1982. matches,
  1983. fogOfWar.active,
  1984. flushSync,
  1985. preventScrollReset,
  1986. submission
  1987. );
  1988. }
  1989. // Call the action for the matched fetcher.submit(), and then handle redirects,
  1990. // errors, and revalidation
  1991. async function handleFetcherAction(
  1992. key: string,
  1993. routeId: string,
  1994. path: string,
  1995. match: AgnosticDataRouteMatch,
  1996. requestMatches: AgnosticDataRouteMatch[],
  1997. isFogOfWar: boolean,
  1998. flushSync: boolean,
  1999. preventScrollReset: boolean,
  2000. submission: Submission
  2001. ) {
  2002. interruptActiveLoads();
  2003. fetchLoadMatches.delete(key);
  2004. function detectAndHandle405Error(m: AgnosticDataRouteMatch) {
  2005. if (!m.route.action && !m.route.lazy) {
  2006. let error = getInternalRouterError(405, {
  2007. method: submission.formMethod,
  2008. pathname: path,
  2009. routeId: routeId,
  2010. });
  2011. setFetcherError(key, routeId, error, { flushSync });
  2012. return true;
  2013. }
  2014. return false;
  2015. }
  2016. if (!isFogOfWar && detectAndHandle405Error(match)) {
  2017. return;
  2018. }
  2019. // Put this fetcher into it's submitting state
  2020. let existingFetcher = state.fetchers.get(key);
  2021. updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
  2022. flushSync,
  2023. });
  2024. let abortController = new AbortController();
  2025. let fetchRequest = createClientSideRequest(
  2026. init.history,
  2027. path,
  2028. abortController.signal,
  2029. submission
  2030. );
  2031. if (isFogOfWar) {
  2032. let discoverResult = await discoverRoutes(
  2033. requestMatches,
  2034. new URL(fetchRequest.url).pathname,
  2035. fetchRequest.signal,
  2036. key
  2037. );
  2038. if (discoverResult.type === "aborted") {
  2039. return;
  2040. } else if (discoverResult.type === "error") {
  2041. setFetcherError(key, routeId, discoverResult.error, { flushSync });
  2042. return;
  2043. } else if (!discoverResult.matches) {
  2044. setFetcherError(
  2045. key,
  2046. routeId,
  2047. getInternalRouterError(404, { pathname: path }),
  2048. { flushSync }
  2049. );
  2050. return;
  2051. } else {
  2052. requestMatches = discoverResult.matches;
  2053. match = getTargetMatch(requestMatches, path);
  2054. if (detectAndHandle405Error(match)) {
  2055. return;
  2056. }
  2057. }
  2058. }
  2059. // Call the action for the fetcher
  2060. fetchControllers.set(key, abortController);
  2061. let originatingLoadId = incrementingLoadId;
  2062. let actionResults = await callDataStrategy(
  2063. "action",
  2064. state,
  2065. fetchRequest,
  2066. [match],
  2067. requestMatches,
  2068. key
  2069. );
  2070. let actionResult = actionResults[match.route.id];
  2071. if (fetchRequest.signal.aborted) {
  2072. // We can delete this so long as we weren't aborted by our own fetcher
  2073. // re-submit which would have put _new_ controller is in fetchControllers
  2074. if (fetchControllers.get(key) === abortController) {
  2075. fetchControllers.delete(key);
  2076. }
  2077. return;
  2078. }
  2079. // When using v7_fetcherPersist, we don't want errors bubbling up to the UI
  2080. // or redirects processed for unmounted fetchers so we just revert them to
  2081. // idle
  2082. if (future.v7_fetcherPersist && deletedFetchers.has(key)) {
  2083. if (isRedirectResult(actionResult) || isErrorResult(actionResult)) {
  2084. updateFetcherState(key, getDoneFetcher(undefined));
  2085. return;
  2086. }
  2087. // Let SuccessResult's fall through for revalidation
  2088. } else {
  2089. if (isRedirectResult(actionResult)) {
  2090. fetchControllers.delete(key);
  2091. if (pendingNavigationLoadId > originatingLoadId) {
  2092. // A new navigation was kicked off after our action started, so that
  2093. // should take precedence over this redirect navigation. We already
  2094. // set isRevalidationRequired so all loaders for the new route should
  2095. // fire unless opted out via shouldRevalidate
  2096. updateFetcherState(key, getDoneFetcher(undefined));
  2097. return;
  2098. } else {
  2099. fetchRedirectIds.add(key);
  2100. updateFetcherState(key, getLoadingFetcher(submission));
  2101. return startRedirectNavigation(fetchRequest, actionResult, false, {
  2102. fetcherSubmission: submission,
  2103. preventScrollReset,
  2104. });
  2105. }
  2106. }
  2107. // Process any non-redirect errors thrown
  2108. if (isErrorResult(actionResult)) {
  2109. setFetcherError(key, routeId, actionResult.error);
  2110. return;
  2111. }
  2112. }
  2113. if (isDeferredResult(actionResult)) {
  2114. throw getInternalRouterError(400, { type: "defer-action" });
  2115. }
  2116. // Start the data load for current matches, or the next location if we're
  2117. // in the middle of a navigation
  2118. let nextLocation = state.navigation.location || state.location;
  2119. let revalidationRequest = createClientSideRequest(
  2120. init.history,
  2121. nextLocation,
  2122. abortController.signal
  2123. );
  2124. let routesToUse = inFlightDataRoutes || dataRoutes;
  2125. let matches =
  2126. state.navigation.state !== "idle"
  2127. ? matchRoutes(routesToUse, state.navigation.location, basename)
  2128. : state.matches;
  2129. invariant(matches, "Didn't find any matches after fetcher action");
  2130. let loadId = ++incrementingLoadId;
  2131. fetchReloadIds.set(key, loadId);
  2132. let loadFetcher = getLoadingFetcher(submission, actionResult.data);
  2133. state.fetchers.set(key, loadFetcher);
  2134. let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
  2135. init.history,
  2136. state,
  2137. matches,
  2138. submission,
  2139. nextLocation,
  2140. false,
  2141. future.v7_skipActionErrorRevalidation,
  2142. isRevalidationRequired,
  2143. cancelledDeferredRoutes,
  2144. cancelledFetcherLoads,
  2145. deletedFetchers,
  2146. fetchLoadMatches,
  2147. fetchRedirectIds,
  2148. routesToUse,
  2149. basename,
  2150. [match.route.id, actionResult]
  2151. );
  2152. // Put all revalidating fetchers into the loading state, except for the
  2153. // current fetcher which we want to keep in it's current loading state which
  2154. // contains it's action submission info + action data
  2155. revalidatingFetchers
  2156. .filter((rf) => rf.key !== key)
  2157. .forEach((rf) => {
  2158. let staleKey = rf.key;
  2159. let existingFetcher = state.fetchers.get(staleKey);
  2160. let revalidatingFetcher = getLoadingFetcher(
  2161. undefined,
  2162. existingFetcher ? existingFetcher.data : undefined
  2163. );
  2164. state.fetchers.set(staleKey, revalidatingFetcher);
  2165. abortFetcher(staleKey);
  2166. if (rf.controller) {
  2167. fetchControllers.set(staleKey, rf.controller);
  2168. }
  2169. });
  2170. updateState({ fetchers: new Map(state.fetchers) });
  2171. let abortPendingFetchRevalidations = () =>
  2172. revalidatingFetchers.forEach((rf) => abortFetcher(rf.key));
  2173. abortController.signal.addEventListener(
  2174. "abort",
  2175. abortPendingFetchRevalidations
  2176. );
  2177. let { loaderResults, fetcherResults } =
  2178. await callLoadersAndMaybeResolveData(
  2179. state,
  2180. matches,
  2181. matchesToLoad,
  2182. revalidatingFetchers,
  2183. revalidationRequest
  2184. );
  2185. if (abortController.signal.aborted) {
  2186. return;
  2187. }
  2188. abortController.signal.removeEventListener(
  2189. "abort",
  2190. abortPendingFetchRevalidations
  2191. );
  2192. fetchReloadIds.delete(key);
  2193. fetchControllers.delete(key);
  2194. revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
  2195. let redirect = findRedirect(loaderResults);
  2196. if (redirect) {
  2197. return startRedirectNavigation(
  2198. revalidationRequest,
  2199. redirect.result,
  2200. false,
  2201. { preventScrollReset }
  2202. );
  2203. }
  2204. redirect = findRedirect(fetcherResults);
  2205. if (redirect) {
  2206. // If this redirect came from a fetcher make sure we mark it in
  2207. // fetchRedirectIds so it doesn't get revalidated on the next set of
  2208. // loader executions
  2209. fetchRedirectIds.add(redirect.key);
  2210. return startRedirectNavigation(
  2211. revalidationRequest,
  2212. redirect.result,
  2213. false,
  2214. { preventScrollReset }
  2215. );
  2216. }
  2217. // Process and commit output from loaders
  2218. let { loaderData, errors } = processLoaderData(
  2219. state,
  2220. matches,
  2221. loaderResults,
  2222. undefined,
  2223. revalidatingFetchers,
  2224. fetcherResults,
  2225. activeDeferreds
  2226. );
  2227. // Since we let revalidations complete even if the submitting fetcher was
  2228. // deleted, only put it back to idle if it hasn't been deleted
  2229. if (state.fetchers.has(key)) {
  2230. let doneFetcher = getDoneFetcher(actionResult.data);
  2231. state.fetchers.set(key, doneFetcher);
  2232. }
  2233. abortStaleFetchLoads(loadId);
  2234. // If we are currently in a navigation loading state and this fetcher is
  2235. // more recent than the navigation, we want the newer data so abort the
  2236. // navigation and complete it with the fetcher data
  2237. if (
  2238. state.navigation.state === "loading" &&
  2239. loadId > pendingNavigationLoadId
  2240. ) {
  2241. invariant(pendingAction, "Expected pending action");
  2242. pendingNavigationController && pendingNavigationController.abort();
  2243. completeNavigation(state.navigation.location, {
  2244. matches,
  2245. loaderData,
  2246. errors,
  2247. fetchers: new Map(state.fetchers),
  2248. });
  2249. } else {
  2250. // otherwise just update with the fetcher data, preserving any existing
  2251. // loaderData for loaders that did not need to reload. We have to
  2252. // manually merge here since we aren't going through completeNavigation
  2253. updateState({
  2254. errors,
  2255. loaderData: mergeLoaderData(
  2256. state.loaderData,
  2257. loaderData,
  2258. matches,
  2259. errors
  2260. ),
  2261. fetchers: new Map(state.fetchers),
  2262. });
  2263. isRevalidationRequired = false;
  2264. }
  2265. }
  2266. // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
  2267. async function handleFetcherLoader(
  2268. key: string,
  2269. routeId: string,
  2270. path: string,
  2271. match: AgnosticDataRouteMatch,
  2272. matches: AgnosticDataRouteMatch[],
  2273. isFogOfWar: boolean,
  2274. flushSync: boolean,
  2275. preventScrollReset: boolean,
  2276. submission?: Submission
  2277. ) {
  2278. let existingFetcher = state.fetchers.get(key);
  2279. updateFetcherState(
  2280. key,
  2281. getLoadingFetcher(
  2282. submission,
  2283. existingFetcher ? existingFetcher.data : undefined
  2284. ),
  2285. { flushSync }
  2286. );
  2287. let abortController = new AbortController();
  2288. let fetchRequest = createClientSideRequest(
  2289. init.history,
  2290. path,
  2291. abortController.signal
  2292. );
  2293. if (isFogOfWar) {
  2294. let discoverResult = await discoverRoutes(
  2295. matches,
  2296. new URL(fetchRequest.url).pathname,
  2297. fetchRequest.signal,
  2298. key
  2299. );
  2300. if (discoverResult.type === "aborted") {
  2301. return;
  2302. } else if (discoverResult.type === "error") {
  2303. setFetcherError(key, routeId, discoverResult.error, { flushSync });
  2304. return;
  2305. } else if (!discoverResult.matches) {
  2306. setFetcherError(
  2307. key,
  2308. routeId,
  2309. getInternalRouterError(404, { pathname: path }),
  2310. { flushSync }
  2311. );
  2312. return;
  2313. } else {
  2314. matches = discoverResult.matches;
  2315. match = getTargetMatch(matches, path);
  2316. }
  2317. }
  2318. // Call the loader for this fetcher route match
  2319. fetchControllers.set(key, abortController);
  2320. let originatingLoadId = incrementingLoadId;
  2321. let results = await callDataStrategy(
  2322. "loader",
  2323. state,
  2324. fetchRequest,
  2325. [match],
  2326. matches,
  2327. key
  2328. );
  2329. let result = results[match.route.id];
  2330. // Deferred isn't supported for fetcher loads, await everything and treat it
  2331. // as a normal load. resolveDeferredData will return undefined if this
  2332. // fetcher gets aborted, so we just leave result untouched and short circuit
  2333. // below if that happens
  2334. if (isDeferredResult(result)) {
  2335. result =
  2336. (await resolveDeferredData(result, fetchRequest.signal, true)) ||
  2337. result;
  2338. }
  2339. // We can delete this so long as we weren't aborted by our our own fetcher
  2340. // re-load which would have put _new_ controller is in fetchControllers
  2341. if (fetchControllers.get(key) === abortController) {
  2342. fetchControllers.delete(key);
  2343. }
  2344. if (fetchRequest.signal.aborted) {
  2345. return;
  2346. }
  2347. // We don't want errors bubbling up or redirects followed for unmounted
  2348. // fetchers, so short circuit here if it was removed from the UI
  2349. if (deletedFetchers.has(key)) {
  2350. updateFetcherState(key, getDoneFetcher(undefined));
  2351. return;
  2352. }
  2353. // If the loader threw a redirect Response, start a new REPLACE navigation
  2354. if (isRedirectResult(result)) {
  2355. if (pendingNavigationLoadId > originatingLoadId) {
  2356. // A new navigation was kicked off after our loader started, so that
  2357. // should take precedence over this redirect navigation
  2358. updateFetcherState(key, getDoneFetcher(undefined));
  2359. return;
  2360. } else {
  2361. fetchRedirectIds.add(key);
  2362. await startRedirectNavigation(fetchRequest, result, false, {
  2363. preventScrollReset,
  2364. });
  2365. return;
  2366. }
  2367. }
  2368. // Process any non-redirect errors thrown
  2369. if (isErrorResult(result)) {
  2370. setFetcherError(key, routeId, result.error);
  2371. return;
  2372. }
  2373. invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
  2374. // Put the fetcher back into an idle state
  2375. updateFetcherState(key, getDoneFetcher(result.data));
  2376. }
  2377. /**
  2378. * Utility function to handle redirects returned from an action or loader.
  2379. * Normally, a redirect "replaces" the navigation that triggered it. So, for
  2380. * example:
  2381. *
  2382. * - user is on /a
  2383. * - user clicks a link to /b
  2384. * - loader for /b redirects to /c
  2385. *
  2386. * In a non-JS app the browser would track the in-flight navigation to /b and
  2387. * then replace it with /c when it encountered the redirect response. In
  2388. * the end it would only ever update the URL bar with /c.
  2389. *
  2390. * In client-side routing using pushState/replaceState, we aim to emulate
  2391. * this behavior and we also do not update history until the end of the
  2392. * navigation (including processed redirects). This means that we never
  2393. * actually touch history until we've processed redirects, so we just use
  2394. * the history action from the original navigation (PUSH or REPLACE).
  2395. */
  2396. async function startRedirectNavigation(
  2397. request: Request,
  2398. redirect: RedirectResult,
  2399. isNavigation: boolean,
  2400. {
  2401. submission,
  2402. fetcherSubmission,
  2403. preventScrollReset,
  2404. replace,
  2405. }: {
  2406. submission?: Submission;
  2407. fetcherSubmission?: Submission;
  2408. preventScrollReset?: boolean;
  2409. replace?: boolean;
  2410. } = {}
  2411. ) {
  2412. if (redirect.response.headers.has("X-Remix-Revalidate")) {
  2413. isRevalidationRequired = true;
  2414. }
  2415. let location = redirect.response.headers.get("Location");
  2416. invariant(location, "Expected a Location header on the redirect Response");
  2417. location = normalizeRedirectLocation(
  2418. location,
  2419. new URL(request.url),
  2420. basename
  2421. );
  2422. let redirectLocation = createLocation(state.location, location, {
  2423. _isRedirect: true,
  2424. });
  2425. if (isBrowser) {
  2426. let isDocumentReload = false;
  2427. if (redirect.response.headers.has("X-Remix-Reload-Document")) {
  2428. // Hard reload if the response contained X-Remix-Reload-Document
  2429. isDocumentReload = true;
  2430. } else if (ABSOLUTE_URL_REGEX.test(location)) {
  2431. const url = init.history.createURL(location);
  2432. isDocumentReload =
  2433. // Hard reload if it's an absolute URL to a new origin
  2434. url.origin !== routerWindow.location.origin ||
  2435. // Hard reload if it's an absolute URL that does not match our basename
  2436. stripBasename(url.pathname, basename) == null;
  2437. }
  2438. if (isDocumentReload) {
  2439. if (replace) {
  2440. routerWindow.location.replace(location);
  2441. } else {
  2442. routerWindow.location.assign(location);
  2443. }
  2444. return;
  2445. }
  2446. }
  2447. // There's no need to abort on redirects, since we don't detect the
  2448. // redirect until the action/loaders have settled
  2449. pendingNavigationController = null;
  2450. let redirectHistoryAction =
  2451. replace === true || redirect.response.headers.has("X-Remix-Replace")
  2452. ? HistoryAction.Replace
  2453. : HistoryAction.Push;
  2454. // Use the incoming submission if provided, fallback on the active one in
  2455. // state.navigation
  2456. let { formMethod, formAction, formEncType } = state.navigation;
  2457. if (
  2458. !submission &&
  2459. !fetcherSubmission &&
  2460. formMethod &&
  2461. formAction &&
  2462. formEncType
  2463. ) {
  2464. submission = getSubmissionFromNavigation(state.navigation);
  2465. }
  2466. // If this was a 307/308 submission we want to preserve the HTTP method and
  2467. // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
  2468. // redirected location
  2469. let activeSubmission = submission || fetcherSubmission;
  2470. if (
  2471. redirectPreserveMethodStatusCodes.has(redirect.response.status) &&
  2472. activeSubmission &&
  2473. isMutationMethod(activeSubmission.formMethod)
  2474. ) {
  2475. await startNavigation(redirectHistoryAction, redirectLocation, {
  2476. submission: {
  2477. ...activeSubmission,
  2478. formAction: location,
  2479. },
  2480. // Preserve these flags across redirects
  2481. preventScrollReset: preventScrollReset || pendingPreventScrollReset,
  2482. enableViewTransition: isNavigation
  2483. ? pendingViewTransitionEnabled
  2484. : undefined,
  2485. });
  2486. } else {
  2487. // If we have a navigation submission, we will preserve it through the
  2488. // redirect navigation
  2489. let overrideNavigation = getLoadingNavigation(
  2490. redirectLocation,
  2491. submission
  2492. );
  2493. await startNavigation(redirectHistoryAction, redirectLocation, {
  2494. overrideNavigation,
  2495. // Send fetcher submissions through for shouldRevalidate
  2496. fetcherSubmission,
  2497. // Preserve these flags across redirects
  2498. preventScrollReset: preventScrollReset || pendingPreventScrollReset,
  2499. enableViewTransition: isNavigation
  2500. ? pendingViewTransitionEnabled
  2501. : undefined,
  2502. });
  2503. }
  2504. }
  2505. // Utility wrapper for calling dataStrategy client-side without having to
  2506. // pass around the manifest, mapRouteProperties, etc.
  2507. async function callDataStrategy(
  2508. type: "loader" | "action",
  2509. state: RouterState,
  2510. request: Request,
  2511. matchesToLoad: AgnosticDataRouteMatch[],
  2512. matches: AgnosticDataRouteMatch[],
  2513. fetcherKey: string | null
  2514. ): Promise<Record<string, DataResult>> {
  2515. let results: Record<string, DataStrategyResult>;
  2516. let dataResults: Record<string, DataResult> = {};
  2517. try {
  2518. results = await callDataStrategyImpl(
  2519. dataStrategyImpl,
  2520. type,
  2521. state,
  2522. request,
  2523. matchesToLoad,
  2524. matches,
  2525. fetcherKey,
  2526. manifest,
  2527. mapRouteProperties
  2528. );
  2529. } catch (e) {
  2530. // If the outer dataStrategy method throws, just return the error for all
  2531. // matches - and it'll naturally bubble to the root
  2532. matchesToLoad.forEach((m) => {
  2533. dataResults[m.route.id] = {
  2534. type: ResultType.error,
  2535. error: e,
  2536. };
  2537. });
  2538. return dataResults;
  2539. }
  2540. for (let [routeId, result] of Object.entries(results)) {
  2541. if (isRedirectDataStrategyResultResult(result)) {
  2542. let response = result.result as Response;
  2543. dataResults[routeId] = {
  2544. type: ResultType.redirect,
  2545. response: normalizeRelativeRoutingRedirectResponse(
  2546. response,
  2547. request,
  2548. routeId,
  2549. matches,
  2550. basename,
  2551. future.v7_relativeSplatPath
  2552. ),
  2553. };
  2554. } else {
  2555. dataResults[routeId] = await convertDataStrategyResultToDataResult(
  2556. result
  2557. );
  2558. }
  2559. }
  2560. return dataResults;
  2561. }
  2562. async function callLoadersAndMaybeResolveData(
  2563. state: RouterState,
  2564. matches: AgnosticDataRouteMatch[],
  2565. matchesToLoad: AgnosticDataRouteMatch[],
  2566. fetchersToLoad: RevalidatingFetcher[],
  2567. request: Request
  2568. ) {
  2569. let currentMatches = state.matches;
  2570. // Kick off loaders and fetchers in parallel
  2571. let loaderResultsPromise = callDataStrategy(
  2572. "loader",
  2573. state,
  2574. request,
  2575. matchesToLoad,
  2576. matches,
  2577. null
  2578. );
  2579. let fetcherResultsPromise = Promise.all(
  2580. fetchersToLoad.map(async (f) => {
  2581. if (f.matches && f.match && f.controller) {
  2582. let results = await callDataStrategy(
  2583. "loader",
  2584. state,
  2585. createClientSideRequest(init.history, f.path, f.controller.signal),
  2586. [f.match],
  2587. f.matches,
  2588. f.key
  2589. );
  2590. let result = results[f.match.route.id];
  2591. // Fetcher results are keyed by fetcher key from here on out, not routeId
  2592. return { [f.key]: result };
  2593. } else {
  2594. return Promise.resolve({
  2595. [f.key]: {
  2596. type: ResultType.error,
  2597. error: getInternalRouterError(404, {
  2598. pathname: f.path,
  2599. }),
  2600. } as ErrorResult,
  2601. });
  2602. }
  2603. })
  2604. );
  2605. let loaderResults = await loaderResultsPromise;
  2606. let fetcherResults = (await fetcherResultsPromise).reduce(
  2607. (acc, r) => Object.assign(acc, r),
  2608. {}
  2609. );
  2610. await Promise.all([
  2611. resolveNavigationDeferredResults(
  2612. matches,
  2613. loaderResults,
  2614. request.signal,
  2615. currentMatches,
  2616. state.loaderData
  2617. ),
  2618. resolveFetcherDeferredResults(matches, fetcherResults, fetchersToLoad),
  2619. ]);
  2620. return {
  2621. loaderResults,
  2622. fetcherResults,
  2623. };
  2624. }
  2625. function interruptActiveLoads() {
  2626. // Every interruption triggers a revalidation
  2627. isRevalidationRequired = true;
  2628. // Cancel pending route-level deferreds and mark cancelled routes for
  2629. // revalidation
  2630. cancelledDeferredRoutes.push(...cancelActiveDeferreds());
  2631. // Abort in-flight fetcher loads
  2632. fetchLoadMatches.forEach((_, key) => {
  2633. if (fetchControllers.has(key)) {
  2634. cancelledFetcherLoads.add(key);
  2635. }
  2636. abortFetcher(key);
  2637. });
  2638. }
  2639. function updateFetcherState(
  2640. key: string,
  2641. fetcher: Fetcher,
  2642. opts: { flushSync?: boolean } = {}
  2643. ) {
  2644. state.fetchers.set(key, fetcher);
  2645. updateState(
  2646. { fetchers: new Map(state.fetchers) },
  2647. { flushSync: (opts && opts.flushSync) === true }
  2648. );
  2649. }
  2650. function setFetcherError(
  2651. key: string,
  2652. routeId: string,
  2653. error: any,
  2654. opts: { flushSync?: boolean } = {}
  2655. ) {
  2656. let boundaryMatch = findNearestBoundary(state.matches, routeId);
  2657. deleteFetcher(key);
  2658. updateState(
  2659. {
  2660. errors: {
  2661. [boundaryMatch.route.id]: error,
  2662. },
  2663. fetchers: new Map(state.fetchers),
  2664. },
  2665. { flushSync: (opts && opts.flushSync) === true }
  2666. );
  2667. }
  2668. function getFetcher<TData = any>(key: string): Fetcher<TData> {
  2669. activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
  2670. // If this fetcher was previously marked for deletion, unmark it since we
  2671. // have a new instance
  2672. if (deletedFetchers.has(key)) {
  2673. deletedFetchers.delete(key);
  2674. }
  2675. return state.fetchers.get(key) || IDLE_FETCHER;
  2676. }
  2677. function deleteFetcher(key: string): void {
  2678. let fetcher = state.fetchers.get(key);
  2679. // Don't abort the controller if this is a deletion of a fetcher.submit()
  2680. // in it's loading phase since - we don't want to abort the corresponding
  2681. // revalidation and want them to complete and land
  2682. if (
  2683. fetchControllers.has(key) &&
  2684. !(fetcher && fetcher.state === "loading" && fetchReloadIds.has(key))
  2685. ) {
  2686. abortFetcher(key);
  2687. }
  2688. fetchLoadMatches.delete(key);
  2689. fetchReloadIds.delete(key);
  2690. fetchRedirectIds.delete(key);
  2691. // If we opted into the flag we can clear this now since we're calling
  2692. // deleteFetcher() at the end of updateState() and we've already handed the
  2693. // deleted fetcher keys off to the data layer.
  2694. // If not, we're eagerly calling deleteFetcher() and we need to keep this
  2695. // Set populated until the next updateState call, and we'll clear
  2696. // `deletedFetchers` then
  2697. if (future.v7_fetcherPersist) {
  2698. deletedFetchers.delete(key);
  2699. }
  2700. cancelledFetcherLoads.delete(key);
  2701. state.fetchers.delete(key);
  2702. }
  2703. function deleteFetcherAndUpdateState(key: string): void {
  2704. let count = (activeFetchers.get(key) || 0) - 1;
  2705. if (count <= 0) {
  2706. activeFetchers.delete(key);
  2707. deletedFetchers.add(key);
  2708. if (!future.v7_fetcherPersist) {
  2709. deleteFetcher(key);
  2710. }
  2711. } else {
  2712. activeFetchers.set(key, count);
  2713. }
  2714. updateState({ fetchers: new Map(state.fetchers) });
  2715. }
  2716. function abortFetcher(key: string) {
  2717. let controller = fetchControllers.get(key);
  2718. if (controller) {
  2719. controller.abort();
  2720. fetchControllers.delete(key);
  2721. }
  2722. }
  2723. function markFetchersDone(keys: string[]) {
  2724. for (let key of keys) {
  2725. let fetcher = getFetcher(key);
  2726. let doneFetcher = getDoneFetcher(fetcher.data);
  2727. state.fetchers.set(key, doneFetcher);
  2728. }
  2729. }
  2730. function markFetchRedirectsDone(): boolean {
  2731. let doneKeys = [];
  2732. let updatedFetchers = false;
  2733. for (let key of fetchRedirectIds) {
  2734. let fetcher = state.fetchers.get(key);
  2735. invariant(fetcher, `Expected fetcher: ${key}`);
  2736. if (fetcher.state === "loading") {
  2737. fetchRedirectIds.delete(key);
  2738. doneKeys.push(key);
  2739. updatedFetchers = true;
  2740. }
  2741. }
  2742. markFetchersDone(doneKeys);
  2743. return updatedFetchers;
  2744. }
  2745. function abortStaleFetchLoads(landedId: number): boolean {
  2746. let yeetedKeys = [];
  2747. for (let [key, id] of fetchReloadIds) {
  2748. if (id < landedId) {
  2749. let fetcher = state.fetchers.get(key);
  2750. invariant(fetcher, `Expected fetcher: ${key}`);
  2751. if (fetcher.state === "loading") {
  2752. abortFetcher(key);
  2753. fetchReloadIds.delete(key);
  2754. yeetedKeys.push(key);
  2755. }
  2756. }
  2757. }
  2758. markFetchersDone(yeetedKeys);
  2759. return yeetedKeys.length > 0;
  2760. }
  2761. function getBlocker(key: string, fn: BlockerFunction) {
  2762. let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
  2763. if (blockerFunctions.get(key) !== fn) {
  2764. blockerFunctions.set(key, fn);
  2765. }
  2766. return blocker;
  2767. }
  2768. function deleteBlocker(key: string) {
  2769. state.blockers.delete(key);
  2770. blockerFunctions.delete(key);
  2771. }
  2772. // Utility function to update blockers, ensuring valid state transitions
  2773. function updateBlocker(key: string, newBlocker: Blocker) {
  2774. let blocker = state.blockers.get(key) || IDLE_BLOCKER;
  2775. // Poor mans state machine :)
  2776. // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
  2777. invariant(
  2778. (blocker.state === "unblocked" && newBlocker.state === "blocked") ||
  2779. (blocker.state === "blocked" && newBlocker.state === "blocked") ||
  2780. (blocker.state === "blocked" && newBlocker.state === "proceeding") ||
  2781. (blocker.state === "blocked" && newBlocker.state === "unblocked") ||
  2782. (blocker.state === "proceeding" && newBlocker.state === "unblocked"),
  2783. `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
  2784. );
  2785. let blockers = new Map(state.blockers);
  2786. blockers.set(key, newBlocker);
  2787. updateState({ blockers });
  2788. }
  2789. function shouldBlockNavigation({
  2790. currentLocation,
  2791. nextLocation,
  2792. historyAction,
  2793. }: {
  2794. currentLocation: Location;
  2795. nextLocation: Location;
  2796. historyAction: HistoryAction;
  2797. }): string | undefined {
  2798. if (blockerFunctions.size === 0) {
  2799. return;
  2800. }
  2801. // We ony support a single active blocker at the moment since we don't have
  2802. // any compelling use cases for multi-blocker yet
  2803. if (blockerFunctions.size > 1) {
  2804. warning(false, "A router only supports one blocker at a time");
  2805. }
  2806. let entries = Array.from(blockerFunctions.entries());
  2807. let [blockerKey, blockerFunction] = entries[entries.length - 1];
  2808. let blocker = state.blockers.get(blockerKey);
  2809. if (blocker && blocker.state === "proceeding") {
  2810. // If the blocker is currently proceeding, we don't need to re-check
  2811. // it and can let this navigation continue
  2812. return;
  2813. }
  2814. // At this point, we know we're unblocked/blocked so we need to check the
  2815. // user-provided blocker function
  2816. if (blockerFunction({ currentLocation, nextLocation, historyAction })) {
  2817. return blockerKey;
  2818. }
  2819. }
  2820. function handleNavigational404(pathname: string) {
  2821. let error = getInternalRouterError(404, { pathname });
  2822. let routesToUse = inFlightDataRoutes || dataRoutes;
  2823. let { matches, route } = getShortCircuitMatches(routesToUse);
  2824. // Cancel all pending deferred on 404s since we don't keep any routes
  2825. cancelActiveDeferreds();
  2826. return { notFoundMatches: matches, route, error };
  2827. }
  2828. function cancelActiveDeferreds(
  2829. predicate?: (routeId: string) => boolean
  2830. ): string[] {
  2831. let cancelledRouteIds: string[] = [];
  2832. activeDeferreds.forEach((dfd, routeId) => {
  2833. if (!predicate || predicate(routeId)) {
  2834. // Cancel the deferred - but do not remove from activeDeferreds here -
  2835. // we rely on the subscribers to do that so our tests can assert proper
  2836. // cleanup via _internalActiveDeferreds
  2837. dfd.cancel();
  2838. cancelledRouteIds.push(routeId);
  2839. activeDeferreds.delete(routeId);
  2840. }
  2841. });
  2842. return cancelledRouteIds;
  2843. }
  2844. // Opt in to capturing and reporting scroll positions during navigations,
  2845. // used by the <ScrollRestoration> component
  2846. function enableScrollRestoration(
  2847. positions: Record<string, number>,
  2848. getPosition: GetScrollPositionFunction,
  2849. getKey?: GetScrollRestorationKeyFunction
  2850. ) {
  2851. savedScrollPositions = positions;
  2852. getScrollPosition = getPosition;
  2853. getScrollRestorationKey = getKey || null;
  2854. // Perform initial hydration scroll restoration, since we miss the boat on
  2855. // the initial updateState() because we've not yet rendered <ScrollRestoration/>
  2856. // and therefore have no savedScrollPositions available
  2857. if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
  2858. initialScrollRestored = true;
  2859. let y = getSavedScrollPosition(state.location, state.matches);
  2860. if (y != null) {
  2861. updateState({ restoreScrollPosition: y });
  2862. }
  2863. }
  2864. return () => {
  2865. savedScrollPositions = null;
  2866. getScrollPosition = null;
  2867. getScrollRestorationKey = null;
  2868. };
  2869. }
  2870. function getScrollKey(location: Location, matches: AgnosticDataRouteMatch[]) {
  2871. if (getScrollRestorationKey) {
  2872. let key = getScrollRestorationKey(
  2873. location,
  2874. matches.map((m) => convertRouteMatchToUiMatch(m, state.loaderData))
  2875. );
  2876. return key || location.key;
  2877. }
  2878. return location.key;
  2879. }
  2880. function saveScrollPosition(
  2881. location: Location,
  2882. matches: AgnosticDataRouteMatch[]
  2883. ): void {
  2884. if (savedScrollPositions && getScrollPosition) {
  2885. let key = getScrollKey(location, matches);
  2886. savedScrollPositions[key] = getScrollPosition();
  2887. }
  2888. }
  2889. function getSavedScrollPosition(
  2890. location: Location,
  2891. matches: AgnosticDataRouteMatch[]
  2892. ): number | null {
  2893. if (savedScrollPositions) {
  2894. let key = getScrollKey(location, matches);
  2895. let y = savedScrollPositions[key];
  2896. if (typeof y === "number") {
  2897. return y;
  2898. }
  2899. }
  2900. return null;
  2901. }
  2902. function checkFogOfWar(
  2903. matches: AgnosticDataRouteMatch[] | null,
  2904. routesToUse: AgnosticDataRouteObject[],
  2905. pathname: string
  2906. ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
  2907. if (patchRoutesOnNavigationImpl) {
  2908. if (!matches) {
  2909. let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(
  2910. routesToUse,
  2911. pathname,
  2912. basename,
  2913. true
  2914. );
  2915. return { active: true, matches: fogMatches || [] };
  2916. } else {
  2917. if (Object.keys(matches[0].params).length > 0) {
  2918. // If we matched a dynamic param or a splat, it might only be because
  2919. // we haven't yet discovered other routes that would match with a
  2920. // higher score. Call patchRoutesOnNavigation just to be sure
  2921. let partialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
  2922. routesToUse,
  2923. pathname,
  2924. basename,
  2925. true
  2926. );
  2927. return { active: true, matches: partialMatches };
  2928. }
  2929. }
  2930. }
  2931. return { active: false, matches: null };
  2932. }
  2933. type DiscoverRoutesSuccessResult = {
  2934. type: "success";
  2935. matches: AgnosticDataRouteMatch[] | null;
  2936. };
  2937. type DiscoverRoutesErrorResult = {
  2938. type: "error";
  2939. error: any;
  2940. partialMatches: AgnosticDataRouteMatch[];
  2941. };
  2942. type DiscoverRoutesAbortedResult = { type: "aborted" };
  2943. type DiscoverRoutesResult =
  2944. | DiscoverRoutesSuccessResult
  2945. | DiscoverRoutesErrorResult
  2946. | DiscoverRoutesAbortedResult;
  2947. async function discoverRoutes(
  2948. matches: AgnosticDataRouteMatch[],
  2949. pathname: string,
  2950. signal: AbortSignal,
  2951. fetcherKey?: string
  2952. ): Promise<DiscoverRoutesResult> {
  2953. if (!patchRoutesOnNavigationImpl) {
  2954. return { type: "success", matches };
  2955. }
  2956. let partialMatches: AgnosticDataRouteMatch[] | null = matches;
  2957. while (true) {
  2958. let isNonHMR = inFlightDataRoutes == null;
  2959. let routesToUse = inFlightDataRoutes || dataRoutes;
  2960. let localManifest = manifest;
  2961. try {
  2962. await patchRoutesOnNavigationImpl({
  2963. signal,
  2964. path: pathname,
  2965. matches: partialMatches,
  2966. fetcherKey,
  2967. patch: (routeId, children) => {
  2968. if (signal.aborted) return;
  2969. patchRoutesImpl(
  2970. routeId,
  2971. children,
  2972. routesToUse,
  2973. localManifest,
  2974. mapRouteProperties
  2975. );
  2976. },
  2977. });
  2978. } catch (e) {
  2979. return { type: "error", error: e, partialMatches };
  2980. } finally {
  2981. // If we are not in the middle of an HMR revalidation and we changed the
  2982. // routes, provide a new identity so when we `updateState` at the end of
  2983. // this navigation/fetch `router.routes` will be a new identity and
  2984. // trigger a re-run of memoized `router.routes` dependencies.
  2985. // HMR will already update the identity and reflow when it lands
  2986. // `inFlightDataRoutes` in `completeNavigation`
  2987. if (isNonHMR && !signal.aborted) {
  2988. dataRoutes = [...dataRoutes];
  2989. }
  2990. }
  2991. if (signal.aborted) {
  2992. return { type: "aborted" };
  2993. }
  2994. let newMatches = matchRoutes(routesToUse, pathname, basename);
  2995. if (newMatches) {
  2996. return { type: "success", matches: newMatches };
  2997. }
  2998. let newPartialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
  2999. routesToUse,
  3000. pathname,
  3001. basename,
  3002. true
  3003. );
  3004. // Avoid loops if the second pass results in the same partial matches
  3005. if (
  3006. !newPartialMatches ||
  3007. (partialMatches.length === newPartialMatches.length &&
  3008. partialMatches.every(
  3009. (m, i) => m.route.id === newPartialMatches![i].route.id
  3010. ))
  3011. ) {
  3012. return { type: "success", matches: null };
  3013. }
  3014. partialMatches = newPartialMatches;
  3015. }
  3016. }
  3017. function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
  3018. manifest = {};
  3019. inFlightDataRoutes = convertRoutesToDataRoutes(
  3020. newRoutes,
  3021. mapRouteProperties,
  3022. undefined,
  3023. manifest
  3024. );
  3025. }
  3026. function patchRoutes(
  3027. routeId: string | null,
  3028. children: AgnosticRouteObject[]
  3029. ): void {
  3030. let isNonHMR = inFlightDataRoutes == null;
  3031. let routesToUse = inFlightDataRoutes || dataRoutes;
  3032. patchRoutesImpl(
  3033. routeId,
  3034. children,
  3035. routesToUse,
  3036. manifest,
  3037. mapRouteProperties
  3038. );
  3039. // If we are not in the middle of an HMR revalidation and we changed the
  3040. // routes, provide a new identity and trigger a reflow via `updateState`
  3041. // to re-run memoized `router.routes` dependencies.
  3042. // HMR will already update the identity and reflow when it lands
  3043. // `inFlightDataRoutes` in `completeNavigation`
  3044. if (isNonHMR) {
  3045. dataRoutes = [...dataRoutes];
  3046. updateState({});
  3047. }
  3048. }
  3049. router = {
  3050. get basename() {
  3051. return basename;
  3052. },
  3053. get future() {
  3054. return future;
  3055. },
  3056. get state() {
  3057. return state;
  3058. },
  3059. get routes() {
  3060. return dataRoutes;
  3061. },
  3062. get window() {
  3063. return routerWindow;
  3064. },
  3065. initialize,
  3066. subscribe,
  3067. enableScrollRestoration,
  3068. navigate,
  3069. fetch,
  3070. revalidate,
  3071. // Passthrough to history-aware createHref used by useHref so we get proper
  3072. // hash-aware URLs in DOM paths
  3073. createHref: (to: To) => init.history.createHref(to),
  3074. encodeLocation: (to: To) => init.history.encodeLocation(to),
  3075. getFetcher,
  3076. deleteFetcher: deleteFetcherAndUpdateState,
  3077. dispose,
  3078. getBlocker,
  3079. deleteBlocker,
  3080. patchRoutes,
  3081. _internalFetchControllers: fetchControllers,
  3082. _internalActiveDeferreds: activeDeferreds,
  3083. // TODO: Remove setRoutes, it's temporary to avoid dealing with
  3084. // updating the tree while validating the update algorithm.
  3085. _internalSetRoutes,
  3086. };
  3087. return router;
  3088. }
  3089. //#endregion
  3090. ////////////////////////////////////////////////////////////////////////////////
  3091. //#region createStaticHandler
  3092. ////////////////////////////////////////////////////////////////////////////////
  3093. export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
  3094. /**
  3095. * Future flags to toggle new feature behavior
  3096. */
  3097. export interface StaticHandlerFutureConfig {
  3098. v7_relativeSplatPath: boolean;
  3099. v7_throwAbortReason: boolean;
  3100. }
  3101. export interface CreateStaticHandlerOptions {
  3102. basename?: string;
  3103. /**
  3104. * @deprecated Use `mapRouteProperties` instead
  3105. */
  3106. detectErrorBoundary?: DetectErrorBoundaryFunction;
  3107. mapRouteProperties?: MapRoutePropertiesFunction;
  3108. future?: Partial<StaticHandlerFutureConfig>;
  3109. }
  3110. export function createStaticHandler(
  3111. routes: AgnosticRouteObject[],
  3112. opts?: CreateStaticHandlerOptions
  3113. ): StaticHandler {
  3114. invariant(
  3115. routes.length > 0,
  3116. "You must provide a non-empty routes array to createStaticHandler"
  3117. );
  3118. let manifest: RouteManifest = {};
  3119. let basename = (opts ? opts.basename : null) || "/";
  3120. let mapRouteProperties: MapRoutePropertiesFunction;
  3121. if (opts?.mapRouteProperties) {
  3122. mapRouteProperties = opts.mapRouteProperties;
  3123. } else if (opts?.detectErrorBoundary) {
  3124. // If they are still using the deprecated version, wrap it with the new API
  3125. let detectErrorBoundary = opts.detectErrorBoundary;
  3126. mapRouteProperties = (route) => ({
  3127. hasErrorBoundary: detectErrorBoundary(route),
  3128. });
  3129. } else {
  3130. mapRouteProperties = defaultMapRouteProperties;
  3131. }
  3132. // Config driven behavior flags
  3133. let future: StaticHandlerFutureConfig = {
  3134. v7_relativeSplatPath: false,
  3135. v7_throwAbortReason: false,
  3136. ...(opts ? opts.future : null),
  3137. };
  3138. let dataRoutes = convertRoutesToDataRoutes(
  3139. routes,
  3140. mapRouteProperties,
  3141. undefined,
  3142. manifest
  3143. );
  3144. /**
  3145. * The query() method is intended for document requests, in which we want to
  3146. * call an optional action and potentially multiple loaders for all nested
  3147. * routes. It returns a StaticHandlerContext object, which is very similar
  3148. * to the router state (location, loaderData, actionData, errors, etc.) and
  3149. * also adds SSR-specific information such as the statusCode and headers
  3150. * from action/loaders Responses.
  3151. *
  3152. * It _should_ never throw and should report all errors through the
  3153. * returned context.errors object, properly associating errors to their error
  3154. * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
  3155. * used to emulate React error boundaries during SSr by performing a second
  3156. * pass only down to the boundaryId.
  3157. *
  3158. * The one exception where we do not return a StaticHandlerContext is when a
  3159. * redirect response is returned or thrown from any action/loader. We
  3160. * propagate that out and return the raw Response so the HTTP server can
  3161. * return it directly.
  3162. *
  3163. * - `opts.requestContext` is an optional server context that will be passed
  3164. * to actions/loaders in the `context` parameter
  3165. * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
  3166. * the bubbling of errors which allows single-fetch-type implementations
  3167. * where the client will handle the bubbling and we may need to return data
  3168. * for the handling route
  3169. */
  3170. async function query(
  3171. request: Request,
  3172. {
  3173. requestContext,
  3174. skipLoaderErrorBubbling,
  3175. dataStrategy,
  3176. }: {
  3177. requestContext?: unknown;
  3178. skipLoaderErrorBubbling?: boolean;
  3179. dataStrategy?: DataStrategyFunction;
  3180. } = {}
  3181. ): Promise<StaticHandlerContext | Response> {
  3182. let url = new URL(request.url);
  3183. let method = request.method;
  3184. let location = createLocation("", createPath(url), null, "default");
  3185. let matches = matchRoutes(dataRoutes, location, basename);
  3186. // SSR supports HEAD requests while SPA doesn't
  3187. if (!isValidMethod(method) && method !== "HEAD") {
  3188. let error = getInternalRouterError(405, { method });
  3189. let { matches: methodNotAllowedMatches, route } =
  3190. getShortCircuitMatches(dataRoutes);
  3191. return {
  3192. basename,
  3193. location,
  3194. matches: methodNotAllowedMatches,
  3195. loaderData: {},
  3196. actionData: null,
  3197. errors: {
  3198. [route.id]: error,
  3199. },
  3200. statusCode: error.status,
  3201. loaderHeaders: {},
  3202. actionHeaders: {},
  3203. activeDeferreds: null,
  3204. };
  3205. } else if (!matches) {
  3206. let error = getInternalRouterError(404, { pathname: location.pathname });
  3207. let { matches: notFoundMatches, route } =
  3208. getShortCircuitMatches(dataRoutes);
  3209. return {
  3210. basename,
  3211. location,
  3212. matches: notFoundMatches,
  3213. loaderData: {},
  3214. actionData: null,
  3215. errors: {
  3216. [route.id]: error,
  3217. },
  3218. statusCode: error.status,
  3219. loaderHeaders: {},
  3220. actionHeaders: {},
  3221. activeDeferreds: null,
  3222. };
  3223. }
  3224. let result = await queryImpl(
  3225. request,
  3226. location,
  3227. matches,
  3228. requestContext,
  3229. dataStrategy || null,
  3230. skipLoaderErrorBubbling === true,
  3231. null
  3232. );
  3233. if (isResponse(result)) {
  3234. return result;
  3235. }
  3236. // When returning StaticHandlerContext, we patch back in the location here
  3237. // since we need it for React Context. But this helps keep our submit and
  3238. // loadRouteData operating on a Request instead of a Location
  3239. return { location, basename, ...result };
  3240. }
  3241. /**
  3242. * The queryRoute() method is intended for targeted route requests, either
  3243. * for fetch ?_data requests or resource route requests. In this case, we
  3244. * are only ever calling a single action or loader, and we are returning the
  3245. * returned value directly. In most cases, this will be a Response returned
  3246. * from the action/loader, but it may be a primitive or other value as well -
  3247. * and in such cases the calling context should handle that accordingly.
  3248. *
  3249. * We do respect the throw/return differentiation, so if an action/loader
  3250. * throws, then this method will throw the value. This is important so we
  3251. * can do proper boundary identification in Remix where a thrown Response
  3252. * must go to the Catch Boundary but a returned Response is happy-path.
  3253. *
  3254. * One thing to note is that any Router-initiated Errors that make sense
  3255. * to associate with a status code will be thrown as an ErrorResponse
  3256. * instance which include the raw Error, such that the calling context can
  3257. * serialize the error as they see fit while including the proper response
  3258. * code. Examples here are 404 and 405 errors that occur prior to reaching
  3259. * any user-defined loaders.
  3260. *
  3261. * - `opts.routeId` allows you to specify the specific route handler to call.
  3262. * If not provided the handler will determine the proper route by matching
  3263. * against `request.url`
  3264. * - `opts.requestContext` is an optional server context that will be passed
  3265. * to actions/loaders in the `context` parameter
  3266. */
  3267. async function queryRoute(
  3268. request: Request,
  3269. {
  3270. routeId,
  3271. requestContext,
  3272. dataStrategy,
  3273. }: {
  3274. requestContext?: unknown;
  3275. routeId?: string;
  3276. dataStrategy?: DataStrategyFunction;
  3277. } = {}
  3278. ): Promise<any> {
  3279. let url = new URL(request.url);
  3280. let method = request.method;
  3281. let location = createLocation("", createPath(url), null, "default");
  3282. let matches = matchRoutes(dataRoutes, location, basename);
  3283. // SSR supports HEAD requests while SPA doesn't
  3284. if (!isValidMethod(method) && method !== "HEAD" && method !== "OPTIONS") {
  3285. throw getInternalRouterError(405, { method });
  3286. } else if (!matches) {
  3287. throw getInternalRouterError(404, { pathname: location.pathname });
  3288. }
  3289. let match = routeId
  3290. ? matches.find((m) => m.route.id === routeId)
  3291. : getTargetMatch(matches, location);
  3292. if (routeId && !match) {
  3293. throw getInternalRouterError(403, {
  3294. pathname: location.pathname,
  3295. routeId,
  3296. });
  3297. } else if (!match) {
  3298. // This should never hit I don't think?
  3299. throw getInternalRouterError(404, { pathname: location.pathname });
  3300. }
  3301. let result = await queryImpl(
  3302. request,
  3303. location,
  3304. matches,
  3305. requestContext,
  3306. dataStrategy || null,
  3307. false,
  3308. match
  3309. );
  3310. if (isResponse(result)) {
  3311. return result;
  3312. }
  3313. let error = result.errors ? Object.values(result.errors)[0] : undefined;
  3314. if (error !== undefined) {
  3315. // If we got back result.errors, that means the loader/action threw
  3316. // _something_ that wasn't a Response, but it's not guaranteed/required
  3317. // to be an `instanceof Error` either, so we have to use throw here to
  3318. // preserve the "error" state outside of queryImpl.
  3319. throw error;
  3320. }
  3321. // Pick off the right state value to return
  3322. if (result.actionData) {
  3323. return Object.values(result.actionData)[0];
  3324. }
  3325. if (result.loaderData) {
  3326. let data = Object.values(result.loaderData)[0];
  3327. if (result.activeDeferreds?.[match.route.id]) {
  3328. data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
  3329. }
  3330. return data;
  3331. }
  3332. return undefined;
  3333. }
  3334. async function queryImpl(
  3335. request: Request,
  3336. location: Location,
  3337. matches: AgnosticDataRouteMatch[],
  3338. requestContext: unknown,
  3339. dataStrategy: DataStrategyFunction | null,
  3340. skipLoaderErrorBubbling: boolean,
  3341. routeMatch: AgnosticDataRouteMatch | null
  3342. ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
  3343. invariant(
  3344. request.signal,
  3345. "query()/queryRoute() requests must contain an AbortController signal"
  3346. );
  3347. try {
  3348. if (isMutationMethod(request.method.toLowerCase())) {
  3349. let result = await submit(
  3350. request,
  3351. matches,
  3352. routeMatch || getTargetMatch(matches, location),
  3353. requestContext,
  3354. dataStrategy,
  3355. skipLoaderErrorBubbling,
  3356. routeMatch != null
  3357. );
  3358. return result;
  3359. }
  3360. let result = await loadRouteData(
  3361. request,
  3362. matches,
  3363. requestContext,
  3364. dataStrategy,
  3365. skipLoaderErrorBubbling,
  3366. routeMatch
  3367. );
  3368. return isResponse(result)
  3369. ? result
  3370. : {
  3371. ...result,
  3372. actionData: null,
  3373. actionHeaders: {},
  3374. };
  3375. } catch (e) {
  3376. // If the user threw/returned a Response in callLoaderOrAction for a
  3377. // `queryRoute` call, we throw the `DataStrategyResult` to bail out early
  3378. // and then return or throw the raw Response here accordingly
  3379. if (isDataStrategyResult(e) && isResponse(e.result)) {
  3380. if (e.type === ResultType.error) {
  3381. throw e.result;
  3382. }
  3383. return e.result;
  3384. }
  3385. // Redirects are always returned since they don't propagate to catch
  3386. // boundaries
  3387. if (isRedirectResponse(e)) {
  3388. return e;
  3389. }
  3390. throw e;
  3391. }
  3392. }
  3393. async function submit(
  3394. request: Request,
  3395. matches: AgnosticDataRouteMatch[],
  3396. actionMatch: AgnosticDataRouteMatch,
  3397. requestContext: unknown,
  3398. dataStrategy: DataStrategyFunction | null,
  3399. skipLoaderErrorBubbling: boolean,
  3400. isRouteRequest: boolean
  3401. ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
  3402. let result: DataResult;
  3403. if (!actionMatch.route.action && !actionMatch.route.lazy) {
  3404. let error = getInternalRouterError(405, {
  3405. method: request.method,
  3406. pathname: new URL(request.url).pathname,
  3407. routeId: actionMatch.route.id,
  3408. });
  3409. if (isRouteRequest) {
  3410. throw error;
  3411. }
  3412. result = {
  3413. type: ResultType.error,
  3414. error,
  3415. };
  3416. } else {
  3417. let results = await callDataStrategy(
  3418. "action",
  3419. request,
  3420. [actionMatch],
  3421. matches,
  3422. isRouteRequest,
  3423. requestContext,
  3424. dataStrategy
  3425. );
  3426. result = results[actionMatch.route.id];
  3427. if (request.signal.aborted) {
  3428. throwStaticHandlerAbortedError(request, isRouteRequest, future);
  3429. }
  3430. }
  3431. if (isRedirectResult(result)) {
  3432. // Uhhhh - this should never happen, we should always throw these from
  3433. // callLoaderOrAction, but the type narrowing here keeps TS happy and we
  3434. // can get back on the "throw all redirect responses" train here should
  3435. // this ever happen :/
  3436. throw new Response(null, {
  3437. status: result.response.status,
  3438. headers: {
  3439. Location: result.response.headers.get("Location")!,
  3440. },
  3441. });
  3442. }
  3443. if (isDeferredResult(result)) {
  3444. let error = getInternalRouterError(400, { type: "defer-action" });
  3445. if (isRouteRequest) {
  3446. throw error;
  3447. }
  3448. result = {
  3449. type: ResultType.error,
  3450. error,
  3451. };
  3452. }
  3453. if (isRouteRequest) {
  3454. // Note: This should only be non-Response values if we get here, since
  3455. // isRouteRequest should throw any Response received in callLoaderOrAction
  3456. if (isErrorResult(result)) {
  3457. throw result.error;
  3458. }
  3459. return {
  3460. matches: [actionMatch],
  3461. loaderData: {},
  3462. actionData: { [actionMatch.route.id]: result.data },
  3463. errors: null,
  3464. // Note: statusCode + headers are unused here since queryRoute will
  3465. // return the raw Response or value
  3466. statusCode: 200,
  3467. loaderHeaders: {},
  3468. actionHeaders: {},
  3469. activeDeferreds: null,
  3470. };
  3471. }
  3472. // Create a GET request for the loaders
  3473. let loaderRequest = new Request(request.url, {
  3474. headers: request.headers,
  3475. redirect: request.redirect,
  3476. signal: request.signal,
  3477. });
  3478. if (isErrorResult(result)) {
  3479. // Store off the pending error - we use it to determine which loaders
  3480. // to call and will commit it when we complete the navigation
  3481. let boundaryMatch = skipLoaderErrorBubbling
  3482. ? actionMatch
  3483. : findNearestBoundary(matches, actionMatch.route.id);
  3484. let context = await loadRouteData(
  3485. loaderRequest,
  3486. matches,
  3487. requestContext,
  3488. dataStrategy,
  3489. skipLoaderErrorBubbling,
  3490. null,
  3491. [boundaryMatch.route.id, result]
  3492. );
  3493. // action status codes take precedence over loader status codes
  3494. return {
  3495. ...context,
  3496. statusCode: isRouteErrorResponse(result.error)
  3497. ? result.error.status
  3498. : result.statusCode != null
  3499. ? result.statusCode
  3500. : 500,
  3501. actionData: null,
  3502. actionHeaders: {
  3503. ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
  3504. },
  3505. };
  3506. }
  3507. let context = await loadRouteData(
  3508. loaderRequest,
  3509. matches,
  3510. requestContext,
  3511. dataStrategy,
  3512. skipLoaderErrorBubbling,
  3513. null
  3514. );
  3515. return {
  3516. ...context,
  3517. actionData: {
  3518. [actionMatch.route.id]: result.data,
  3519. },
  3520. // action status codes take precedence over loader status codes
  3521. ...(result.statusCode ? { statusCode: result.statusCode } : {}),
  3522. actionHeaders: result.headers
  3523. ? { [actionMatch.route.id]: result.headers }
  3524. : {},
  3525. };
  3526. }
  3527. async function loadRouteData(
  3528. request: Request,
  3529. matches: AgnosticDataRouteMatch[],
  3530. requestContext: unknown,
  3531. dataStrategy: DataStrategyFunction | null,
  3532. skipLoaderErrorBubbling: boolean,
  3533. routeMatch: AgnosticDataRouteMatch | null,
  3534. pendingActionResult?: PendingActionResult
  3535. ): Promise<
  3536. | Omit<
  3537. StaticHandlerContext,
  3538. "location" | "basename" | "actionData" | "actionHeaders"
  3539. >
  3540. | Response
  3541. > {
  3542. let isRouteRequest = routeMatch != null;
  3543. // Short circuit if we have no loaders to run (queryRoute())
  3544. if (
  3545. isRouteRequest &&
  3546. !routeMatch?.route.loader &&
  3547. !routeMatch?.route.lazy
  3548. ) {
  3549. throw getInternalRouterError(400, {
  3550. method: request.method,
  3551. pathname: new URL(request.url).pathname,
  3552. routeId: routeMatch?.route.id,
  3553. });
  3554. }
  3555. let requestMatches = routeMatch
  3556. ? [routeMatch]
  3557. : pendingActionResult && isErrorResult(pendingActionResult[1])
  3558. ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0])
  3559. : matches;
  3560. let matchesToLoad = requestMatches.filter(
  3561. (m) => m.route.loader || m.route.lazy
  3562. );
  3563. // Short circuit if we have no loaders to run (query())
  3564. if (matchesToLoad.length === 0) {
  3565. return {
  3566. matches,
  3567. // Add a null for all matched routes for proper revalidation on the client
  3568. loaderData: matches.reduce(
  3569. (acc, m) => Object.assign(acc, { [m.route.id]: null }),
  3570. {}
  3571. ),
  3572. errors:
  3573. pendingActionResult && isErrorResult(pendingActionResult[1])
  3574. ? {
  3575. [pendingActionResult[0]]: pendingActionResult[1].error,
  3576. }
  3577. : null,
  3578. statusCode: 200,
  3579. loaderHeaders: {},
  3580. activeDeferreds: null,
  3581. };
  3582. }
  3583. let results = await callDataStrategy(
  3584. "loader",
  3585. request,
  3586. matchesToLoad,
  3587. matches,
  3588. isRouteRequest,
  3589. requestContext,
  3590. dataStrategy
  3591. );
  3592. if (request.signal.aborted) {
  3593. throwStaticHandlerAbortedError(request, isRouteRequest, future);
  3594. }
  3595. // Process and commit output from loaders
  3596. let activeDeferreds = new Map<string, DeferredData>();
  3597. let context = processRouteLoaderData(
  3598. matches,
  3599. results,
  3600. pendingActionResult,
  3601. activeDeferreds,
  3602. skipLoaderErrorBubbling
  3603. );
  3604. // Add a null for any non-loader matches for proper revalidation on the client
  3605. let executedLoaders = new Set<string>(
  3606. matchesToLoad.map((match) => match.route.id)
  3607. );
  3608. matches.forEach((match) => {
  3609. if (!executedLoaders.has(match.route.id)) {
  3610. context.loaderData[match.route.id] = null;
  3611. }
  3612. });
  3613. return {
  3614. ...context,
  3615. matches,
  3616. activeDeferreds:
  3617. activeDeferreds.size > 0
  3618. ? Object.fromEntries(activeDeferreds.entries())
  3619. : null,
  3620. };
  3621. }
  3622. // Utility wrapper for calling dataStrategy server-side without having to
  3623. // pass around the manifest, mapRouteProperties, etc.
  3624. async function callDataStrategy(
  3625. type: "loader" | "action",
  3626. request: Request,
  3627. matchesToLoad: AgnosticDataRouteMatch[],
  3628. matches: AgnosticDataRouteMatch[],
  3629. isRouteRequest: boolean,
  3630. requestContext: unknown,
  3631. dataStrategy: DataStrategyFunction | null
  3632. ): Promise<Record<string, DataResult>> {
  3633. let results = await callDataStrategyImpl(
  3634. dataStrategy || defaultDataStrategy,
  3635. type,
  3636. null,
  3637. request,
  3638. matchesToLoad,
  3639. matches,
  3640. null,
  3641. manifest,
  3642. mapRouteProperties,
  3643. requestContext
  3644. );
  3645. let dataResults: Record<string, DataResult> = {};
  3646. await Promise.all(
  3647. matches.map(async (match) => {
  3648. if (!(match.route.id in results)) {
  3649. return;
  3650. }
  3651. let result = results[match.route.id];
  3652. if (isRedirectDataStrategyResultResult(result)) {
  3653. let response = result.result as Response;
  3654. // Throw redirects and let the server handle them with an HTTP redirect
  3655. throw normalizeRelativeRoutingRedirectResponse(
  3656. response,
  3657. request,
  3658. match.route.id,
  3659. matches,
  3660. basename,
  3661. future.v7_relativeSplatPath
  3662. );
  3663. }
  3664. if (isResponse(result.result) && isRouteRequest) {
  3665. // For SSR single-route requests, we want to hand Responses back
  3666. // directly without unwrapping
  3667. throw result;
  3668. }
  3669. dataResults[match.route.id] =
  3670. await convertDataStrategyResultToDataResult(result);
  3671. })
  3672. );
  3673. return dataResults;
  3674. }
  3675. return {
  3676. dataRoutes,
  3677. query,
  3678. queryRoute,
  3679. };
  3680. }
  3681. //#endregion
  3682. ////////////////////////////////////////////////////////////////////////////////
  3683. //#region Helpers
  3684. ////////////////////////////////////////////////////////////////////////////////
  3685. /**
  3686. * Given an existing StaticHandlerContext and an error thrown at render time,
  3687. * provide an updated StaticHandlerContext suitable for a second SSR render
  3688. */
  3689. export function getStaticContextFromError(
  3690. routes: AgnosticDataRouteObject[],
  3691. context: StaticHandlerContext,
  3692. error: any
  3693. ) {
  3694. let newContext: StaticHandlerContext = {
  3695. ...context,
  3696. statusCode: isRouteErrorResponse(error) ? error.status : 500,
  3697. errors: {
  3698. [context._deepestRenderedBoundaryId || routes[0].id]: error,
  3699. },
  3700. };
  3701. return newContext;
  3702. }
  3703. function throwStaticHandlerAbortedError(
  3704. request: Request,
  3705. isRouteRequest: boolean,
  3706. future: StaticHandlerFutureConfig
  3707. ) {
  3708. if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
  3709. throw request.signal.reason;
  3710. }
  3711. let method = isRouteRequest ? "queryRoute" : "query";
  3712. throw new Error(`${method}() call aborted: ${request.method} ${request.url}`);
  3713. }
  3714. function isSubmissionNavigation(
  3715. opts: BaseNavigateOrFetchOptions
  3716. ): opts is SubmissionNavigateOptions {
  3717. return (
  3718. opts != null &&
  3719. (("formData" in opts && opts.formData != null) ||
  3720. ("body" in opts && opts.body !== undefined))
  3721. );
  3722. }
  3723. function normalizeTo(
  3724. location: Path,
  3725. matches: AgnosticDataRouteMatch[],
  3726. basename: string,
  3727. prependBasename: boolean,
  3728. to: To | null,
  3729. v7_relativeSplatPath: boolean,
  3730. fromRouteId?: string,
  3731. relative?: RelativeRoutingType
  3732. ) {
  3733. let contextualMatches: AgnosticDataRouteMatch[];
  3734. let activeRouteMatch: AgnosticDataRouteMatch | undefined;
  3735. if (fromRouteId) {
  3736. // Grab matches up to the calling route so our route-relative logic is
  3737. // relative to the correct source route
  3738. contextualMatches = [];
  3739. for (let match of matches) {
  3740. contextualMatches.push(match);
  3741. if (match.route.id === fromRouteId) {
  3742. activeRouteMatch = match;
  3743. break;
  3744. }
  3745. }
  3746. } else {
  3747. contextualMatches = matches;
  3748. activeRouteMatch = matches[matches.length - 1];
  3749. }
  3750. // Resolve the relative path
  3751. let path = resolveTo(
  3752. to ? to : ".",
  3753. getResolveToMatches(contextualMatches, v7_relativeSplatPath),
  3754. stripBasename(location.pathname, basename) || location.pathname,
  3755. relative === "path"
  3756. );
  3757. // When `to` is not specified we inherit search/hash from the current
  3758. // location, unlike when to="." and we just inherit the path.
  3759. // See https://github.com/remix-run/remix/issues/927
  3760. if (to == null) {
  3761. path.search = location.search;
  3762. path.hash = location.hash;
  3763. }
  3764. // Account for `?index` params when routing to the current location
  3765. if ((to == null || to === "" || to === ".") && activeRouteMatch) {
  3766. let nakedIndex = hasNakedIndexQuery(path.search);
  3767. if (activeRouteMatch.route.index && !nakedIndex) {
  3768. // Add one when we're targeting an index route
  3769. path.search = path.search
  3770. ? path.search.replace(/^\?/, "?index&")
  3771. : "?index";
  3772. } else if (!activeRouteMatch.route.index && nakedIndex) {
  3773. // Remove existing ones when we're not
  3774. let params = new URLSearchParams(path.search);
  3775. let indexValues = params.getAll("index");
  3776. params.delete("index");
  3777. indexValues.filter((v) => v).forEach((v) => params.append("index", v));
  3778. let qs = params.toString();
  3779. path.search = qs ? `?${qs}` : "";
  3780. }
  3781. }
  3782. // If we're operating within a basename, prepend it to the pathname. If
  3783. // this is a root navigation, then just use the raw basename which allows
  3784. // the basename to have full control over the presence of a trailing slash
  3785. // on root actions
  3786. if (prependBasename && basename !== "/") {
  3787. path.pathname =
  3788. path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
  3789. }
  3790. return createPath(path);
  3791. }
  3792. // Normalize navigation options by converting formMethod=GET formData objects to
  3793. // URLSearchParams so they behave identically to links with query params
  3794. function normalizeNavigateOptions(
  3795. normalizeFormMethod: boolean,
  3796. isFetcher: boolean,
  3797. path: string,
  3798. opts?: BaseNavigateOrFetchOptions
  3799. ): {
  3800. path: string;
  3801. submission?: Submission;
  3802. error?: ErrorResponseImpl;
  3803. } {
  3804. // Return location verbatim on non-submission navigations
  3805. if (!opts || !isSubmissionNavigation(opts)) {
  3806. return { path };
  3807. }
  3808. if (opts.formMethod && !isValidMethod(opts.formMethod)) {
  3809. return {
  3810. path,
  3811. error: getInternalRouterError(405, { method: opts.formMethod }),
  3812. };
  3813. }
  3814. let getInvalidBodyError = () => ({
  3815. path,
  3816. error: getInternalRouterError(400, { type: "invalid-body" }),
  3817. });
  3818. // Create a Submission on non-GET navigations
  3819. let rawFormMethod = opts.formMethod || "get";
  3820. let formMethod = normalizeFormMethod
  3821. ? (rawFormMethod.toUpperCase() as V7_FormMethod)
  3822. : (rawFormMethod.toLowerCase() as FormMethod);
  3823. let formAction = stripHashFromPath(path);
  3824. if (opts.body !== undefined) {
  3825. if (opts.formEncType === "text/plain") {
  3826. // text only support POST/PUT/PATCH/DELETE submissions
  3827. if (!isMutationMethod(formMethod)) {
  3828. return getInvalidBodyError();
  3829. }
  3830. let text =
  3831. typeof opts.body === "string"
  3832. ? opts.body
  3833. : opts.body instanceof FormData ||
  3834. opts.body instanceof URLSearchParams
  3835. ? // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
  3836. Array.from(opts.body.entries()).reduce(
  3837. (acc, [name, value]) => `${acc}${name}=${value}\n`,
  3838. ""
  3839. )
  3840. : String(opts.body);
  3841. return {
  3842. path,
  3843. submission: {
  3844. formMethod,
  3845. formAction,
  3846. formEncType: opts.formEncType,
  3847. formData: undefined,
  3848. json: undefined,
  3849. text,
  3850. },
  3851. };
  3852. } else if (opts.formEncType === "application/json") {
  3853. // json only supports POST/PUT/PATCH/DELETE submissions
  3854. if (!isMutationMethod(formMethod)) {
  3855. return getInvalidBodyError();
  3856. }
  3857. try {
  3858. let json =
  3859. typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
  3860. return {
  3861. path,
  3862. submission: {
  3863. formMethod,
  3864. formAction,
  3865. formEncType: opts.formEncType,
  3866. formData: undefined,
  3867. json,
  3868. text: undefined,
  3869. },
  3870. };
  3871. } catch (e) {
  3872. return getInvalidBodyError();
  3873. }
  3874. }
  3875. }
  3876. invariant(
  3877. typeof FormData === "function",
  3878. "FormData is not available in this environment"
  3879. );
  3880. let searchParams: URLSearchParams;
  3881. let formData: FormData;
  3882. if (opts.formData) {
  3883. searchParams = convertFormDataToSearchParams(opts.formData);
  3884. formData = opts.formData;
  3885. } else if (opts.body instanceof FormData) {
  3886. searchParams = convertFormDataToSearchParams(opts.body);
  3887. formData = opts.body;
  3888. } else if (opts.body instanceof URLSearchParams) {
  3889. searchParams = opts.body;
  3890. formData = convertSearchParamsToFormData(searchParams);
  3891. } else if (opts.body == null) {
  3892. searchParams = new URLSearchParams();
  3893. formData = new FormData();
  3894. } else {
  3895. try {
  3896. searchParams = new URLSearchParams(opts.body);
  3897. formData = convertSearchParamsToFormData(searchParams);
  3898. } catch (e) {
  3899. return getInvalidBodyError();
  3900. }
  3901. }
  3902. let submission: Submission = {
  3903. formMethod,
  3904. formAction,
  3905. formEncType:
  3906. (opts && opts.formEncType) || "application/x-www-form-urlencoded",
  3907. formData,
  3908. json: undefined,
  3909. text: undefined,
  3910. };
  3911. if (isMutationMethod(submission.formMethod)) {
  3912. return { path, submission };
  3913. }
  3914. // Flatten submission onto URLSearchParams for GET submissions
  3915. let parsedPath = parsePath(path);
  3916. // On GET navigation submissions we can drop the ?index param from the
  3917. // resulting location since all loaders will run. But fetcher GET submissions
  3918. // only run a single loader so we need to preserve any incoming ?index params
  3919. if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
  3920. searchParams.append("index", "");
  3921. }
  3922. parsedPath.search = `?${searchParams}`;
  3923. return { path: createPath(parsedPath), submission };
  3924. }
  3925. // Filter out all routes at/below any caught error as they aren't going to
  3926. // render so we don't need to load them
  3927. function getLoaderMatchesUntilBoundary(
  3928. matches: AgnosticDataRouteMatch[],
  3929. boundaryId: string,
  3930. includeBoundary = false
  3931. ) {
  3932. let index = matches.findIndex((m) => m.route.id === boundaryId);
  3933. if (index >= 0) {
  3934. return matches.slice(0, includeBoundary ? index + 1 : index);
  3935. }
  3936. return matches;
  3937. }
  3938. function getMatchesToLoad(
  3939. history: History,
  3940. state: RouterState,
  3941. matches: AgnosticDataRouteMatch[],
  3942. submission: Submission | undefined,
  3943. location: Location,
  3944. initialHydration: boolean,
  3945. skipActionErrorRevalidation: boolean,
  3946. isRevalidationRequired: boolean,
  3947. cancelledDeferredRoutes: string[],
  3948. cancelledFetcherLoads: Set<string>,
  3949. deletedFetchers: Set<string>,
  3950. fetchLoadMatches: Map<string, FetchLoadMatch>,
  3951. fetchRedirectIds: Set<string>,
  3952. routesToUse: AgnosticDataRouteObject[],
  3953. basename: string | undefined,
  3954. pendingActionResult?: PendingActionResult
  3955. ): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
  3956. let actionResult = pendingActionResult
  3957. ? isErrorResult(pendingActionResult[1])
  3958. ? pendingActionResult[1].error
  3959. : pendingActionResult[1].data
  3960. : undefined;
  3961. let currentUrl = history.createURL(state.location);
  3962. let nextUrl = history.createURL(location);
  3963. // Pick navigation matches that are net-new or qualify for revalidation
  3964. let boundaryMatches = matches;
  3965. if (initialHydration && state.errors) {
  3966. // On initial hydration, only consider matches up to _and including_ the boundary.
  3967. // This is inclusive to handle cases where a server loader ran successfully,
  3968. // a child server loader bubbled up to this route, but this route has
  3969. // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
  3970. // we have a complete version of `loaderData`
  3971. boundaryMatches = getLoaderMatchesUntilBoundary(
  3972. matches,
  3973. Object.keys(state.errors)[0],
  3974. true
  3975. );
  3976. } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {
  3977. // If an action threw an error, we call loaders up to, but not including the
  3978. // boundary
  3979. boundaryMatches = getLoaderMatchesUntilBoundary(
  3980. matches,
  3981. pendingActionResult[0]
  3982. );
  3983. }
  3984. // Don't revalidate loaders by default after action 4xx/5xx responses
  3985. // when the flag is enabled. They can still opt-into revalidation via
  3986. // `shouldRevalidate` via `actionResult`
  3987. let actionStatus = pendingActionResult
  3988. ? pendingActionResult[1].statusCode
  3989. : undefined;
  3990. let shouldSkipRevalidation =
  3991. skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
  3992. let navigationMatches = boundaryMatches.filter((match, index) => {
  3993. let { route } = match;
  3994. if (route.lazy) {
  3995. // We haven't loaded this route yet so we don't know if it's got a loader!
  3996. return true;
  3997. }
  3998. if (route.loader == null) {
  3999. return false;
  4000. }
  4001. if (initialHydration) {
  4002. return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
  4003. }
  4004. // Always call the loader on new route instances and pending defer cancellations
  4005. if (
  4006. isNewLoader(state.loaderData, state.matches[index], match) ||
  4007. cancelledDeferredRoutes.some((id) => id === match.route.id)
  4008. ) {
  4009. return true;
  4010. }
  4011. // This is the default implementation for when we revalidate. If the route
  4012. // provides it's own implementation, then we give them full control but
  4013. // provide this value so they can leverage it if needed after they check
  4014. // their own specific use cases
  4015. let currentRouteMatch = state.matches[index];
  4016. let nextRouteMatch = match;
  4017. return shouldRevalidateLoader(match, {
  4018. currentUrl,
  4019. currentParams: currentRouteMatch.params,
  4020. nextUrl,
  4021. nextParams: nextRouteMatch.params,
  4022. ...submission,
  4023. actionResult,
  4024. actionStatus,
  4025. defaultShouldRevalidate: shouldSkipRevalidation
  4026. ? false
  4027. : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
  4028. isRevalidationRequired ||
  4029. currentUrl.pathname + currentUrl.search ===
  4030. nextUrl.pathname + nextUrl.search ||
  4031. // Search params affect all loaders
  4032. currentUrl.search !== nextUrl.search ||
  4033. isNewRouteInstance(currentRouteMatch, nextRouteMatch),
  4034. });
  4035. });
  4036. // Pick fetcher.loads that need to be revalidated
  4037. let revalidatingFetchers: RevalidatingFetcher[] = [];
  4038. fetchLoadMatches.forEach((f, key) => {
  4039. // Don't revalidate:
  4040. // - on initial hydration (shouldn't be any fetchers then anyway)
  4041. // - if fetcher won't be present in the subsequent render
  4042. // - no longer matches the URL (v7_fetcherPersist=false)
  4043. // - was unmounted but persisted due to v7_fetcherPersist=true
  4044. if (
  4045. initialHydration ||
  4046. !matches.some((m) => m.route.id === f.routeId) ||
  4047. deletedFetchers.has(key)
  4048. ) {
  4049. return;
  4050. }
  4051. let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
  4052. // If the fetcher path no longer matches, push it in with null matches so
  4053. // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is
  4054. // currently only a use-case for Remix HMR where the route tree can change
  4055. // at runtime and remove a route previously loaded via a fetcher
  4056. if (!fetcherMatches) {
  4057. revalidatingFetchers.push({
  4058. key,
  4059. routeId: f.routeId,
  4060. path: f.path,
  4061. matches: null,
  4062. match: null,
  4063. controller: null,
  4064. });
  4065. return;
  4066. }
  4067. // Revalidating fetchers are decoupled from the route matches since they
  4068. // load from a static href. They revalidate based on explicit revalidation
  4069. // (submission, useRevalidator, or X-Remix-Revalidate)
  4070. let fetcher = state.fetchers.get(key);
  4071. let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
  4072. let shouldRevalidate = false;
  4073. if (fetchRedirectIds.has(key)) {
  4074. // Never trigger a revalidation of an actively redirecting fetcher
  4075. shouldRevalidate = false;
  4076. } else if (cancelledFetcherLoads.has(key)) {
  4077. // Always mark for revalidation if the fetcher was cancelled
  4078. cancelledFetcherLoads.delete(key);
  4079. shouldRevalidate = true;
  4080. } else if (
  4081. fetcher &&
  4082. fetcher.state !== "idle" &&
  4083. fetcher.data === undefined
  4084. ) {
  4085. // If the fetcher hasn't ever completed loading yet, then this isn't a
  4086. // revalidation, it would just be a brand new load if an explicit
  4087. // revalidation is required
  4088. shouldRevalidate = isRevalidationRequired;
  4089. } else {
  4090. // Otherwise fall back on any user-defined shouldRevalidate, defaulting
  4091. // to explicit revalidations only
  4092. shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
  4093. currentUrl,
  4094. currentParams: state.matches[state.matches.length - 1].params,
  4095. nextUrl,
  4096. nextParams: matches[matches.length - 1].params,
  4097. ...submission,
  4098. actionResult,
  4099. actionStatus,
  4100. defaultShouldRevalidate: shouldSkipRevalidation
  4101. ? false
  4102. : isRevalidationRequired,
  4103. });
  4104. }
  4105. if (shouldRevalidate) {
  4106. revalidatingFetchers.push({
  4107. key,
  4108. routeId: f.routeId,
  4109. path: f.path,
  4110. matches: fetcherMatches,
  4111. match: fetcherMatch,
  4112. controller: new AbortController(),
  4113. });
  4114. }
  4115. });
  4116. return [navigationMatches, revalidatingFetchers];
  4117. }
  4118. function shouldLoadRouteOnHydration(
  4119. route: AgnosticDataRouteObject,
  4120. loaderData: RouteData | null | undefined,
  4121. errors: RouteData | null | undefined
  4122. ) {
  4123. // We dunno if we have a loader - gotta find out!
  4124. if (route.lazy) {
  4125. return true;
  4126. }
  4127. // No loader, nothing to initialize
  4128. if (!route.loader) {
  4129. return false;
  4130. }
  4131. let hasData = loaderData != null && loaderData[route.id] !== undefined;
  4132. let hasError = errors != null && errors[route.id] !== undefined;
  4133. // Don't run if we error'd during SSR
  4134. if (!hasData && hasError) {
  4135. return false;
  4136. }
  4137. // Explicitly opting-in to running on hydration
  4138. if (typeof route.loader === "function" && route.loader.hydrate === true) {
  4139. return true;
  4140. }
  4141. // Otherwise, run if we're not yet initialized with anything
  4142. return !hasData && !hasError;
  4143. }
  4144. function isNewLoader(
  4145. currentLoaderData: RouteData,
  4146. currentMatch: AgnosticDataRouteMatch,
  4147. match: AgnosticDataRouteMatch
  4148. ) {
  4149. let isNew =
  4150. // [a] -> [a, b]
  4151. !currentMatch ||
  4152. // [a, b] -> [a, c]
  4153. match.route.id !== currentMatch.route.id;
  4154. // Handle the case that we don't have data for a re-used route, potentially
  4155. // from a prior error or from a cancelled pending deferred
  4156. let isMissingData = currentLoaderData[match.route.id] === undefined;
  4157. // Always load if this is a net-new route or we don't yet have data
  4158. return isNew || isMissingData;
  4159. }
  4160. function isNewRouteInstance(
  4161. currentMatch: AgnosticDataRouteMatch,
  4162. match: AgnosticDataRouteMatch
  4163. ) {
  4164. let currentPath = currentMatch.route.path;
  4165. return (
  4166. // param change for this match, /users/123 -> /users/456
  4167. currentMatch.pathname !== match.pathname ||
  4168. // splat param changed, which is not present in match.path
  4169. // e.g. /files/images/avatar.jpg -> files/finances.xls
  4170. (currentPath != null &&
  4171. currentPath.endsWith("*") &&
  4172. currentMatch.params["*"] !== match.params["*"])
  4173. );
  4174. }
  4175. function shouldRevalidateLoader(
  4176. loaderMatch: AgnosticDataRouteMatch,
  4177. arg: ShouldRevalidateFunctionArgs
  4178. ) {
  4179. if (loaderMatch.route.shouldRevalidate) {
  4180. let routeChoice = loaderMatch.route.shouldRevalidate(arg);
  4181. if (typeof routeChoice === "boolean") {
  4182. return routeChoice;
  4183. }
  4184. }
  4185. return arg.defaultShouldRevalidate;
  4186. }
  4187. function patchRoutesImpl(
  4188. routeId: string | null,
  4189. children: AgnosticRouteObject[],
  4190. routesToUse: AgnosticDataRouteObject[],
  4191. manifest: RouteManifest,
  4192. mapRouteProperties: MapRoutePropertiesFunction
  4193. ) {
  4194. let childrenToPatch: AgnosticDataRouteObject[];
  4195. if (routeId) {
  4196. let route = manifest[routeId];
  4197. invariant(
  4198. route,
  4199. `No route found to patch children into: routeId = ${routeId}`
  4200. );
  4201. if (!route.children) {
  4202. route.children = [];
  4203. }
  4204. childrenToPatch = route.children;
  4205. } else {
  4206. childrenToPatch = routesToUse;
  4207. }
  4208. // Don't patch in routes we already know about so that `patch` is idempotent
  4209. // to simplify user-land code. This is useful because we re-call the
  4210. // `patchRoutesOnNavigation` function for matched routes with params.
  4211. let uniqueChildren = children.filter(
  4212. (newRoute) =>
  4213. !childrenToPatch.some((existingRoute) =>
  4214. isSameRoute(newRoute, existingRoute)
  4215. )
  4216. );
  4217. let newRoutes = convertRoutesToDataRoutes(
  4218. uniqueChildren,
  4219. mapRouteProperties,
  4220. [routeId || "_", "patch", String(childrenToPatch?.length || "0")],
  4221. manifest
  4222. );
  4223. childrenToPatch.push(...newRoutes);
  4224. }
  4225. function isSameRoute(
  4226. newRoute: AgnosticRouteObject,
  4227. existingRoute: AgnosticRouteObject
  4228. ): boolean {
  4229. // Most optimal check is by id
  4230. if (
  4231. "id" in newRoute &&
  4232. "id" in existingRoute &&
  4233. newRoute.id === existingRoute.id
  4234. ) {
  4235. return true;
  4236. }
  4237. // Second is by pathing differences
  4238. if (
  4239. !(
  4240. newRoute.index === existingRoute.index &&
  4241. newRoute.path === existingRoute.path &&
  4242. newRoute.caseSensitive === existingRoute.caseSensitive
  4243. )
  4244. ) {
  4245. return false;
  4246. }
  4247. // Pathless layout routes are trickier since we need to check children.
  4248. // If they have no children then they're the same as far as we can tell
  4249. if (
  4250. (!newRoute.children || newRoute.children.length === 0) &&
  4251. (!existingRoute.children || existingRoute.children.length === 0)
  4252. ) {
  4253. return true;
  4254. }
  4255. // Otherwise, we look to see if every child in the new route is already
  4256. // represented in the existing route's children
  4257. return newRoute.children!.every((aChild, i) =>
  4258. existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild))
  4259. );
  4260. }
  4261. /**
  4262. * Execute route.lazy() methods to lazily load route modules (loader, action,
  4263. * shouldRevalidate) and update the routeManifest in place which shares objects
  4264. * with dataRoutes so those get updated as well.
  4265. */
  4266. async function loadLazyRouteModule(
  4267. route: AgnosticDataRouteObject,
  4268. mapRouteProperties: MapRoutePropertiesFunction,
  4269. manifest: RouteManifest
  4270. ) {
  4271. if (!route.lazy) {
  4272. return;
  4273. }
  4274. let lazyRoute = await route.lazy();
  4275. // If the lazy route function was executed and removed by another parallel
  4276. // call then we can return - first lazy() to finish wins because the return
  4277. // value of lazy is expected to be static
  4278. if (!route.lazy) {
  4279. return;
  4280. }
  4281. let routeToUpdate = manifest[route.id];
  4282. invariant(routeToUpdate, "No route found in manifest");
  4283. // Update the route in place. This should be safe because there's no way
  4284. // we could yet be sitting on this route as we can't get there without
  4285. // resolving lazy() first.
  4286. //
  4287. // This is different than the HMR "update" use-case where we may actively be
  4288. // on the route being updated. The main concern boils down to "does this
  4289. // mutation affect any ongoing navigations or any current state.matches
  4290. // values?". If not, it should be safe to update in place.
  4291. let routeUpdates: Record<string, any> = {};
  4292. for (let lazyRouteProperty in lazyRoute) {
  4293. let staticRouteValue =
  4294. routeToUpdate[lazyRouteProperty as keyof typeof routeToUpdate];
  4295. let isPropertyStaticallyDefined =
  4296. staticRouteValue !== undefined &&
  4297. // This property isn't static since it should always be updated based
  4298. // on the route updates
  4299. lazyRouteProperty !== "hasErrorBoundary";
  4300. warning(
  4301. !isPropertyStaticallyDefined,
  4302. `Route "${routeToUpdate.id}" has a static property "${lazyRouteProperty}" ` +
  4303. `defined but its lazy function is also returning a value for this property. ` +
  4304. `The lazy route property "${lazyRouteProperty}" will be ignored.`
  4305. );
  4306. if (
  4307. !isPropertyStaticallyDefined &&
  4308. !immutableRouteKeys.has(lazyRouteProperty as ImmutableRouteKey)
  4309. ) {
  4310. routeUpdates[lazyRouteProperty] =
  4311. lazyRoute[lazyRouteProperty as keyof typeof lazyRoute];
  4312. }
  4313. }
  4314. // Mutate the route with the provided updates. Do this first so we pass
  4315. // the updated version to mapRouteProperties
  4316. Object.assign(routeToUpdate, routeUpdates);
  4317. // Mutate the `hasErrorBoundary` property on the route based on the route
  4318. // updates and remove the `lazy` function so we don't resolve the lazy
  4319. // route again.
  4320. Object.assign(routeToUpdate, {
  4321. // To keep things framework agnostic, we use the provided
  4322. // `mapRouteProperties` (or wrapped `detectErrorBoundary`) function to
  4323. // set the framework-aware properties (`element`/`hasErrorBoundary`) since
  4324. // the logic will differ between frameworks.
  4325. ...mapRouteProperties(routeToUpdate),
  4326. lazy: undefined,
  4327. });
  4328. }
  4329. // Default implementation of `dataStrategy` which fetches all loaders in parallel
  4330. async function defaultDataStrategy({
  4331. matches,
  4332. }: DataStrategyFunctionArgs): ReturnType<DataStrategyFunction> {
  4333. let matchesToLoad = matches.filter((m) => m.shouldLoad);
  4334. let results = await Promise.all(matchesToLoad.map((m) => m.resolve()));
  4335. return results.reduce(
  4336. (acc, result, i) =>
  4337. Object.assign(acc, { [matchesToLoad[i].route.id]: result }),
  4338. {}
  4339. );
  4340. }
  4341. async function callDataStrategyImpl(
  4342. dataStrategyImpl: DataStrategyFunction,
  4343. type: "loader" | "action",
  4344. state: RouterState | null,
  4345. request: Request,
  4346. matchesToLoad: AgnosticDataRouteMatch[],
  4347. matches: AgnosticDataRouteMatch[],
  4348. fetcherKey: string | null,
  4349. manifest: RouteManifest,
  4350. mapRouteProperties: MapRoutePropertiesFunction,
  4351. requestContext?: unknown
  4352. ): Promise<Record<string, DataStrategyResult>> {
  4353. let loadRouteDefinitionsPromises = matches.map((m) =>
  4354. m.route.lazy
  4355. ? loadLazyRouteModule(m.route, mapRouteProperties, manifest)
  4356. : undefined
  4357. );
  4358. let dsMatches = matches.map((match, i) => {
  4359. let loadRoutePromise = loadRouteDefinitionsPromises[i];
  4360. let shouldLoad = matchesToLoad.some((m) => m.route.id === match.route.id);
  4361. // `resolve` encapsulates route.lazy(), executing the loader/action,
  4362. // and mapping return values/thrown errors to a `DataStrategyResult`. Users
  4363. // can pass a callback to take fine-grained control over the execution
  4364. // of the loader/action
  4365. let resolve: DataStrategyMatch["resolve"] = async (handlerOverride) => {
  4366. if (
  4367. handlerOverride &&
  4368. request.method === "GET" &&
  4369. (match.route.lazy || match.route.loader)
  4370. ) {
  4371. shouldLoad = true;
  4372. }
  4373. return shouldLoad
  4374. ? callLoaderOrAction(
  4375. type,
  4376. request,
  4377. match,
  4378. loadRoutePromise,
  4379. handlerOverride,
  4380. requestContext
  4381. )
  4382. : Promise.resolve({ type: ResultType.data, result: undefined });
  4383. };
  4384. return {
  4385. ...match,
  4386. shouldLoad,
  4387. resolve,
  4388. };
  4389. });
  4390. // Send all matches here to allow for a middleware-type implementation.
  4391. // handler will be a no-op for unneeded routes and we filter those results
  4392. // back out below.
  4393. let results = await dataStrategyImpl({
  4394. matches: dsMatches,
  4395. request,
  4396. params: matches[0].params,
  4397. fetcherKey,
  4398. context: requestContext,
  4399. });
  4400. // Wait for all routes to load here but 'swallow the error since we want
  4401. // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -
  4402. // called from `match.resolve()`
  4403. try {
  4404. await Promise.all(loadRouteDefinitionsPromises);
  4405. } catch (e) {
  4406. // No-op
  4407. }
  4408. return results;
  4409. }
  4410. // Default logic for calling a loader/action is the user has no specified a dataStrategy
  4411. async function callLoaderOrAction(
  4412. type: "loader" | "action",
  4413. request: Request,
  4414. match: AgnosticDataRouteMatch,
  4415. loadRoutePromise: Promise<void> | undefined,
  4416. handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
  4417. staticContext?: unknown
  4418. ): Promise<DataStrategyResult> {
  4419. let result: DataStrategyResult;
  4420. let onReject: (() => void) | undefined;
  4421. let runHandler = (
  4422. handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
  4423. ): Promise<DataStrategyResult> => {
  4424. // Setup a promise we can race against so that abort signals short circuit
  4425. let reject: () => void;
  4426. // This will never resolve so safe to type it as Promise<DataStrategyResult> to
  4427. // satisfy the function return value
  4428. let abortPromise = new Promise<DataStrategyResult>((_, r) => (reject = r));
  4429. onReject = () => reject();
  4430. request.signal.addEventListener("abort", onReject);
  4431. let actualHandler = (ctx?: unknown) => {
  4432. if (typeof handler !== "function") {
  4433. return Promise.reject(
  4434. new Error(
  4435. `You cannot call the handler for a route which defines a boolean ` +
  4436. `"${type}" [routeId: ${match.route.id}]`
  4437. )
  4438. );
  4439. }
  4440. return handler(
  4441. {
  4442. request,
  4443. params: match.params,
  4444. context: staticContext,
  4445. },
  4446. ...(ctx !== undefined ? [ctx] : [])
  4447. );
  4448. };
  4449. let handlerPromise: Promise<DataStrategyResult> = (async () => {
  4450. try {
  4451. let val = await (handlerOverride
  4452. ? handlerOverride((ctx: unknown) => actualHandler(ctx))
  4453. : actualHandler());
  4454. return { type: "data", result: val };
  4455. } catch (e) {
  4456. return { type: "error", result: e };
  4457. }
  4458. })();
  4459. return Promise.race([handlerPromise, abortPromise]);
  4460. };
  4461. try {
  4462. let handler = match.route[type];
  4463. // If we have a route.lazy promise, await that first
  4464. if (loadRoutePromise) {
  4465. if (handler) {
  4466. // Run statically defined handler in parallel with lazy()
  4467. let handlerError;
  4468. let [value] = await Promise.all([
  4469. // If the handler throws, don't let it immediately bubble out,
  4470. // since we need to let the lazy() execution finish so we know if this
  4471. // route has a boundary that can handle the error
  4472. runHandler(handler).catch((e) => {
  4473. handlerError = e;
  4474. }),
  4475. loadRoutePromise,
  4476. ]);
  4477. if (handlerError !== undefined) {
  4478. throw handlerError;
  4479. }
  4480. result = value!;
  4481. } else {
  4482. // Load lazy route module, then run any returned handler
  4483. await loadRoutePromise;
  4484. handler = match.route[type];
  4485. if (handler) {
  4486. // Handler still runs even if we got interrupted to maintain consistency
  4487. // with un-abortable behavior of handler execution on non-lazy or
  4488. // previously-lazy-loaded routes
  4489. result = await runHandler(handler);
  4490. } else if (type === "action") {
  4491. let url = new URL(request.url);
  4492. let pathname = url.pathname + url.search;
  4493. throw getInternalRouterError(405, {
  4494. method: request.method,
  4495. pathname,
  4496. routeId: match.route.id,
  4497. });
  4498. } else {
  4499. // lazy() route has no loader to run. Short circuit here so we don't
  4500. // hit the invariant below that errors on returning undefined.
  4501. return { type: ResultType.data, result: undefined };
  4502. }
  4503. }
  4504. } else if (!handler) {
  4505. let url = new URL(request.url);
  4506. let pathname = url.pathname + url.search;
  4507. throw getInternalRouterError(404, {
  4508. pathname,
  4509. });
  4510. } else {
  4511. result = await runHandler(handler);
  4512. }
  4513. invariant(
  4514. result.result !== undefined,
  4515. `You defined ${type === "action" ? "an action" : "a loader"} for route ` +
  4516. `"${match.route.id}" but didn't return anything from your \`${type}\` ` +
  4517. `function. Please return a value or \`null\`.`
  4518. );
  4519. } catch (e) {
  4520. // We should already be catching and converting normal handler executions to
  4521. // DataStrategyResults and returning them, so anything that throws here is an
  4522. // unexpected error we still need to wrap
  4523. return { type: ResultType.error, result: e };
  4524. } finally {
  4525. if (onReject) {
  4526. request.signal.removeEventListener("abort", onReject);
  4527. }
  4528. }
  4529. return result;
  4530. }
  4531. async function convertDataStrategyResultToDataResult(
  4532. dataStrategyResult: DataStrategyResult
  4533. ): Promise<DataResult> {
  4534. let { result, type } = dataStrategyResult;
  4535. if (isResponse(result)) {
  4536. let data: any;
  4537. try {
  4538. let contentType = result.headers.get("Content-Type");
  4539. // Check between word boundaries instead of startsWith() due to the last
  4540. // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
  4541. if (contentType && /\bapplication\/json\b/.test(contentType)) {
  4542. if (result.body == null) {
  4543. data = null;
  4544. } else {
  4545. data = await result.json();
  4546. }
  4547. } else {
  4548. data = await result.text();
  4549. }
  4550. } catch (e) {
  4551. return { type: ResultType.error, error: e };
  4552. }
  4553. if (type === ResultType.error) {
  4554. return {
  4555. type: ResultType.error,
  4556. error: new ErrorResponseImpl(result.status, result.statusText, data),
  4557. statusCode: result.status,
  4558. headers: result.headers,
  4559. };
  4560. }
  4561. return {
  4562. type: ResultType.data,
  4563. data,
  4564. statusCode: result.status,
  4565. headers: result.headers,
  4566. };
  4567. }
  4568. if (type === ResultType.error) {
  4569. if (isDataWithResponseInit(result)) {
  4570. if (result.data instanceof Error) {
  4571. return {
  4572. type: ResultType.error,
  4573. error: result.data,
  4574. statusCode: result.init?.status,
  4575. headers: result.init?.headers
  4576. ? new Headers(result.init.headers)
  4577. : undefined,
  4578. };
  4579. }
  4580. // Convert thrown data() to ErrorResponse instances
  4581. return {
  4582. type: ResultType.error,
  4583. error: new ErrorResponseImpl(
  4584. result.init?.status || 500,
  4585. undefined,
  4586. result.data
  4587. ),
  4588. statusCode: isRouteErrorResponse(result) ? result.status : undefined,
  4589. headers: result.init?.headers
  4590. ? new Headers(result.init.headers)
  4591. : undefined,
  4592. };
  4593. }
  4594. return {
  4595. type: ResultType.error,
  4596. error: result,
  4597. statusCode: isRouteErrorResponse(result) ? result.status : undefined,
  4598. };
  4599. }
  4600. if (isDeferredData(result)) {
  4601. return {
  4602. type: ResultType.deferred,
  4603. deferredData: result,
  4604. statusCode: result.init?.status,
  4605. headers: result.init?.headers && new Headers(result.init.headers),
  4606. };
  4607. }
  4608. if (isDataWithResponseInit(result)) {
  4609. return {
  4610. type: ResultType.data,
  4611. data: result.data,
  4612. statusCode: result.init?.status,
  4613. headers: result.init?.headers
  4614. ? new Headers(result.init.headers)
  4615. : undefined,
  4616. };
  4617. }
  4618. return { type: ResultType.data, data: result };
  4619. }
  4620. // Support relative routing in internal redirects
  4621. function normalizeRelativeRoutingRedirectResponse(
  4622. response: Response,
  4623. request: Request,
  4624. routeId: string,
  4625. matches: AgnosticDataRouteMatch[],
  4626. basename: string,
  4627. v7_relativeSplatPath: boolean
  4628. ) {
  4629. let location = response.headers.get("Location");
  4630. invariant(
  4631. location,
  4632. "Redirects returned/thrown from loaders/actions must have a Location header"
  4633. );
  4634. if (!ABSOLUTE_URL_REGEX.test(location)) {
  4635. let trimmedMatches = matches.slice(
  4636. 0,
  4637. matches.findIndex((m) => m.route.id === routeId) + 1
  4638. );
  4639. location = normalizeTo(
  4640. new URL(request.url),
  4641. trimmedMatches,
  4642. basename,
  4643. true,
  4644. location,
  4645. v7_relativeSplatPath
  4646. );
  4647. response.headers.set("Location", location);
  4648. }
  4649. return response;
  4650. }
  4651. function normalizeRedirectLocation(
  4652. location: string,
  4653. currentUrl: URL,
  4654. basename: string
  4655. ): string {
  4656. if (ABSOLUTE_URL_REGEX.test(location)) {
  4657. // Strip off the protocol+origin for same-origin + same-basename absolute redirects
  4658. let normalizedLocation = location;
  4659. let url = normalizedLocation.startsWith("//")
  4660. ? new URL(currentUrl.protocol + normalizedLocation)
  4661. : new URL(normalizedLocation);
  4662. let isSameBasename = stripBasename(url.pathname, basename) != null;
  4663. if (url.origin === currentUrl.origin && isSameBasename) {
  4664. return url.pathname + url.search + url.hash;
  4665. }
  4666. }
  4667. return location;
  4668. }
  4669. // Utility method for creating the Request instances for loaders/actions during
  4670. // client-side navigations and fetches. During SSR we will always have a
  4671. // Request instance from the static handler (query/queryRoute)
  4672. function createClientSideRequest(
  4673. history: History,
  4674. location: string | Location,
  4675. signal: AbortSignal,
  4676. submission?: Submission
  4677. ): Request {
  4678. let url = history.createURL(stripHashFromPath(location)).toString();
  4679. let init: RequestInit = { signal };
  4680. if (submission && isMutationMethod(submission.formMethod)) {
  4681. let { formMethod, formEncType } = submission;
  4682. // Didn't think we needed this but it turns out unlike other methods, patch
  4683. // won't be properly normalized to uppercase and results in a 405 error.
  4684. // See: https://fetch.spec.whatwg.org/#concept-method
  4685. init.method = formMethod.toUpperCase();
  4686. if (formEncType === "application/json") {
  4687. init.headers = new Headers({ "Content-Type": formEncType });
  4688. init.body = JSON.stringify(submission.json);
  4689. } else if (formEncType === "text/plain") {
  4690. // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
  4691. init.body = submission.text;
  4692. } else if (
  4693. formEncType === "application/x-www-form-urlencoded" &&
  4694. submission.formData
  4695. ) {
  4696. // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
  4697. init.body = convertFormDataToSearchParams(submission.formData);
  4698. } else {
  4699. // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
  4700. init.body = submission.formData;
  4701. }
  4702. }
  4703. return new Request(url, init);
  4704. }
  4705. function convertFormDataToSearchParams(formData: FormData): URLSearchParams {
  4706. let searchParams = new URLSearchParams();
  4707. for (let [key, value] of formData.entries()) {
  4708. // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
  4709. searchParams.append(key, typeof value === "string" ? value : value.name);
  4710. }
  4711. return searchParams;
  4712. }
  4713. function convertSearchParamsToFormData(
  4714. searchParams: URLSearchParams
  4715. ): FormData {
  4716. let formData = new FormData();
  4717. for (let [key, value] of searchParams.entries()) {
  4718. formData.append(key, value);
  4719. }
  4720. return formData;
  4721. }
  4722. function processRouteLoaderData(
  4723. matches: AgnosticDataRouteMatch[],
  4724. results: Record<string, DataResult>,
  4725. pendingActionResult: PendingActionResult | undefined,
  4726. activeDeferreds: Map<string, DeferredData>,
  4727. skipLoaderErrorBubbling: boolean
  4728. ): {
  4729. loaderData: RouterState["loaderData"];
  4730. errors: RouterState["errors"] | null;
  4731. statusCode: number;
  4732. loaderHeaders: Record<string, Headers>;
  4733. } {
  4734. // Fill in loaderData/errors from our loaders
  4735. let loaderData: RouterState["loaderData"] = {};
  4736. let errors: RouterState["errors"] | null = null;
  4737. let statusCode: number | undefined;
  4738. let foundError = false;
  4739. let loaderHeaders: Record<string, Headers> = {};
  4740. let pendingError =
  4741. pendingActionResult && isErrorResult(pendingActionResult[1])
  4742. ? pendingActionResult[1].error
  4743. : undefined;
  4744. // Process loader results into state.loaderData/state.errors
  4745. matches.forEach((match) => {
  4746. if (!(match.route.id in results)) {
  4747. return;
  4748. }
  4749. let id = match.route.id;
  4750. let result = results[id];
  4751. invariant(
  4752. !isRedirectResult(result),
  4753. "Cannot handle redirect results in processLoaderData"
  4754. );
  4755. if (isErrorResult(result)) {
  4756. let error = result.error;
  4757. // If we have a pending action error, we report it at the highest-route
  4758. // that throws a loader error, and then clear it out to indicate that
  4759. // it was consumed
  4760. if (pendingError !== undefined) {
  4761. error = pendingError;
  4762. pendingError = undefined;
  4763. }
  4764. errors = errors || {};
  4765. if (skipLoaderErrorBubbling) {
  4766. errors[id] = error;
  4767. } else {
  4768. // Look upwards from the matched route for the closest ancestor error
  4769. // boundary, defaulting to the root match. Prefer higher error values
  4770. // if lower errors bubble to the same boundary
  4771. let boundaryMatch = findNearestBoundary(matches, id);
  4772. if (errors[boundaryMatch.route.id] == null) {
  4773. errors[boundaryMatch.route.id] = error;
  4774. }
  4775. }
  4776. // Clear our any prior loaderData for the throwing route
  4777. loaderData[id] = undefined;
  4778. // Once we find our first (highest) error, we set the status code and
  4779. // prevent deeper status codes from overriding
  4780. if (!foundError) {
  4781. foundError = true;
  4782. statusCode = isRouteErrorResponse(result.error)
  4783. ? result.error.status
  4784. : 500;
  4785. }
  4786. if (result.headers) {
  4787. loaderHeaders[id] = result.headers;
  4788. }
  4789. } else {
  4790. if (isDeferredResult(result)) {
  4791. activeDeferreds.set(id, result.deferredData);
  4792. loaderData[id] = result.deferredData.data;
  4793. // Error status codes always override success status codes, but if all
  4794. // loaders are successful we take the deepest status code.
  4795. if (
  4796. result.statusCode != null &&
  4797. result.statusCode !== 200 &&
  4798. !foundError
  4799. ) {
  4800. statusCode = result.statusCode;
  4801. }
  4802. if (result.headers) {
  4803. loaderHeaders[id] = result.headers;
  4804. }
  4805. } else {
  4806. loaderData[id] = result.data;
  4807. // Error status codes always override success status codes, but if all
  4808. // loaders are successful we take the deepest status code.
  4809. if (result.statusCode && result.statusCode !== 200 && !foundError) {
  4810. statusCode = result.statusCode;
  4811. }
  4812. if (result.headers) {
  4813. loaderHeaders[id] = result.headers;
  4814. }
  4815. }
  4816. }
  4817. });
  4818. // If we didn't consume the pending action error (i.e., all loaders
  4819. // resolved), then consume it here. Also clear out any loaderData for the
  4820. // throwing route
  4821. if (pendingError !== undefined && pendingActionResult) {
  4822. errors = { [pendingActionResult[0]]: pendingError };
  4823. loaderData[pendingActionResult[0]] = undefined;
  4824. }
  4825. return {
  4826. loaderData,
  4827. errors,
  4828. statusCode: statusCode || 200,
  4829. loaderHeaders,
  4830. };
  4831. }
  4832. function processLoaderData(
  4833. state: RouterState,
  4834. matches: AgnosticDataRouteMatch[],
  4835. results: Record<string, DataResult>,
  4836. pendingActionResult: PendingActionResult | undefined,
  4837. revalidatingFetchers: RevalidatingFetcher[],
  4838. fetcherResults: Record<string, DataResult>,
  4839. activeDeferreds: Map<string, DeferredData>
  4840. ): {
  4841. loaderData: RouterState["loaderData"];
  4842. errors?: RouterState["errors"];
  4843. } {
  4844. let { loaderData, errors } = processRouteLoaderData(
  4845. matches,
  4846. results,
  4847. pendingActionResult,
  4848. activeDeferreds,
  4849. false // This method is only called client side so we always want to bubble
  4850. );
  4851. // Process results from our revalidating fetchers
  4852. revalidatingFetchers.forEach((rf) => {
  4853. let { key, match, controller } = rf;
  4854. let result = fetcherResults[key];
  4855. invariant(result, "Did not find corresponding fetcher result");
  4856. // Process fetcher non-redirect errors
  4857. if (controller && controller.signal.aborted) {
  4858. // Nothing to do for aborted fetchers
  4859. return;
  4860. } else if (isErrorResult(result)) {
  4861. let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
  4862. if (!(errors && errors[boundaryMatch.route.id])) {
  4863. errors = {
  4864. ...errors,
  4865. [boundaryMatch.route.id]: result.error,
  4866. };
  4867. }
  4868. state.fetchers.delete(key);
  4869. } else if (isRedirectResult(result)) {
  4870. // Should never get here, redirects should get processed above, but we
  4871. // keep this to type narrow to a success result in the else
  4872. invariant(false, "Unhandled fetcher revalidation redirect");
  4873. } else if (isDeferredResult(result)) {
  4874. // Should never get here, deferred data should be awaited for fetchers
  4875. // in resolveDeferredResults
  4876. invariant(false, "Unhandled fetcher deferred data");
  4877. } else {
  4878. let doneFetcher = getDoneFetcher(result.data);
  4879. state.fetchers.set(key, doneFetcher);
  4880. }
  4881. });
  4882. return { loaderData, errors };
  4883. }
  4884. function mergeLoaderData(
  4885. loaderData: RouteData,
  4886. newLoaderData: RouteData,
  4887. matches: AgnosticDataRouteMatch[],
  4888. errors: RouteData | null | undefined
  4889. ): RouteData {
  4890. let mergedLoaderData = { ...newLoaderData };
  4891. for (let match of matches) {
  4892. let id = match.route.id;
  4893. if (newLoaderData.hasOwnProperty(id)) {
  4894. if (newLoaderData[id] !== undefined) {
  4895. mergedLoaderData[id] = newLoaderData[id];
  4896. } else {
  4897. // No-op - this is so we ignore existing data if we have a key in the
  4898. // incoming object with an undefined value, which is how we unset a prior
  4899. // loaderData if we encounter a loader error
  4900. }
  4901. } else if (loaderData[id] !== undefined && match.route.loader) {
  4902. // Preserve existing keys not included in newLoaderData and where a loader
  4903. // wasn't removed by HMR
  4904. mergedLoaderData[id] = loaderData[id];
  4905. }
  4906. if (errors && errors.hasOwnProperty(id)) {
  4907. // Don't keep any loader data below the boundary
  4908. break;
  4909. }
  4910. }
  4911. return mergedLoaderData;
  4912. }
  4913. function getActionDataForCommit(
  4914. pendingActionResult: PendingActionResult | undefined
  4915. ) {
  4916. if (!pendingActionResult) {
  4917. return {};
  4918. }
  4919. return isErrorResult(pendingActionResult[1])
  4920. ? {
  4921. // Clear out prior actionData on errors
  4922. actionData: {},
  4923. }
  4924. : {
  4925. actionData: {
  4926. [pendingActionResult[0]]: pendingActionResult[1].data,
  4927. },
  4928. };
  4929. }
  4930. // Find the nearest error boundary, looking upwards from the leaf route (or the
  4931. // route specified by routeId) for the closest ancestor error boundary,
  4932. // defaulting to the root match
  4933. function findNearestBoundary(
  4934. matches: AgnosticDataRouteMatch[],
  4935. routeId?: string
  4936. ): AgnosticDataRouteMatch {
  4937. let eligibleMatches = routeId
  4938. ? matches.slice(0, matches.findIndex((m) => m.route.id === routeId) + 1)
  4939. : [...matches];
  4940. return (
  4941. eligibleMatches.reverse().find((m) => m.route.hasErrorBoundary === true) ||
  4942. matches[0]
  4943. );
  4944. }
  4945. function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
  4946. matches: AgnosticDataRouteMatch[];
  4947. route: AgnosticDataRouteObject;
  4948. } {
  4949. // Prefer a root layout route if present, otherwise shim in a route object
  4950. let route =
  4951. routes.length === 1
  4952. ? routes[0]
  4953. : routes.find((r) => r.index || !r.path || r.path === "/") || {
  4954. id: `__shim-error-route__`,
  4955. };
  4956. return {
  4957. matches: [
  4958. {
  4959. params: {},
  4960. pathname: "",
  4961. pathnameBase: "",
  4962. route,
  4963. },
  4964. ],
  4965. route,
  4966. };
  4967. }
  4968. function getInternalRouterError(
  4969. status: number,
  4970. {
  4971. pathname,
  4972. routeId,
  4973. method,
  4974. type,
  4975. message,
  4976. }: {
  4977. pathname?: string;
  4978. routeId?: string;
  4979. method?: string;
  4980. type?: "defer-action" | "invalid-body";
  4981. message?: string;
  4982. } = {}
  4983. ) {
  4984. let statusText = "Unknown Server Error";
  4985. let errorMessage = "Unknown @remix-run/router error";
  4986. if (status === 400) {
  4987. statusText = "Bad Request";
  4988. if (method && pathname && routeId) {
  4989. errorMessage =
  4990. `You made a ${method} request to "${pathname}" but ` +
  4991. `did not provide a \`loader\` for route "${routeId}", ` +
  4992. `so there is no way to handle the request.`;
  4993. } else if (type === "defer-action") {
  4994. errorMessage = "defer() is not supported in actions";
  4995. } else if (type === "invalid-body") {
  4996. errorMessage = "Unable to encode submission body";
  4997. }
  4998. } else if (status === 403) {
  4999. statusText = "Forbidden";
  5000. errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
  5001. } else if (status === 404) {
  5002. statusText = "Not Found";
  5003. errorMessage = `No route matches URL "${pathname}"`;
  5004. } else if (status === 405) {
  5005. statusText = "Method Not Allowed";
  5006. if (method && pathname && routeId) {
  5007. errorMessage =
  5008. `You made a ${method.toUpperCase()} request to "${pathname}" but ` +
  5009. `did not provide an \`action\` for route "${routeId}", ` +
  5010. `so there is no way to handle the request.`;
  5011. } else if (method) {
  5012. errorMessage = `Invalid request method "${method.toUpperCase()}"`;
  5013. }
  5014. }
  5015. return new ErrorResponseImpl(
  5016. status || 500,
  5017. statusText,
  5018. new Error(errorMessage),
  5019. true
  5020. );
  5021. }
  5022. // Find any returned redirect errors, starting from the lowest match
  5023. function findRedirect(
  5024. results: Record<string, DataResult>
  5025. ): { key: string; result: RedirectResult } | undefined {
  5026. let entries = Object.entries(results);
  5027. for (let i = entries.length - 1; i >= 0; i--) {
  5028. let [key, result] = entries[i];
  5029. if (isRedirectResult(result)) {
  5030. return { key, result };
  5031. }
  5032. }
  5033. }
  5034. function stripHashFromPath(path: To) {
  5035. let parsedPath = typeof path === "string" ? parsePath(path) : path;
  5036. return createPath({ ...parsedPath, hash: "" });
  5037. }
  5038. function isHashChangeOnly(a: Location, b: Location): boolean {
  5039. if (a.pathname !== b.pathname || a.search !== b.search) {
  5040. return false;
  5041. }
  5042. if (a.hash === "") {
  5043. // /page -> /page#hash
  5044. return b.hash !== "";
  5045. } else if (a.hash === b.hash) {
  5046. // /page#hash -> /page#hash
  5047. return true;
  5048. } else if (b.hash !== "") {
  5049. // /page#hash -> /page#other
  5050. return true;
  5051. }
  5052. // If the hash is removed the browser will re-perform a request to the server
  5053. // /page#hash -> /page
  5054. return false;
  5055. }
  5056. function isPromise<T = unknown>(val: unknown): val is Promise<T> {
  5057. return typeof val === "object" && val != null && "then" in val;
  5058. }
  5059. function isDataStrategyResult(result: unknown): result is DataStrategyResult {
  5060. return (
  5061. result != null &&
  5062. typeof result === "object" &&
  5063. "type" in result &&
  5064. "result" in result &&
  5065. (result.type === ResultType.data || result.type === ResultType.error)
  5066. );
  5067. }
  5068. function isRedirectDataStrategyResultResult(result: DataStrategyResult) {
  5069. return (
  5070. isResponse(result.result) && redirectStatusCodes.has(result.result.status)
  5071. );
  5072. }
  5073. function isDeferredResult(result: DataResult): result is DeferredResult {
  5074. return result.type === ResultType.deferred;
  5075. }
  5076. function isErrorResult(result: DataResult): result is ErrorResult {
  5077. return result.type === ResultType.error;
  5078. }
  5079. function isRedirectResult(result?: DataResult): result is RedirectResult {
  5080. return (result && result.type) === ResultType.redirect;
  5081. }
  5082. export function isDataWithResponseInit(
  5083. value: any
  5084. ): value is DataWithResponseInit<unknown> {
  5085. return (
  5086. typeof value === "object" &&
  5087. value != null &&
  5088. "type" in value &&
  5089. "data" in value &&
  5090. "init" in value &&
  5091. value.type === "DataWithResponseInit"
  5092. );
  5093. }
  5094. export function isDeferredData(value: any): value is DeferredData {
  5095. let deferred: DeferredData = value;
  5096. return (
  5097. deferred &&
  5098. typeof deferred === "object" &&
  5099. typeof deferred.data === "object" &&
  5100. typeof deferred.subscribe === "function" &&
  5101. typeof deferred.cancel === "function" &&
  5102. typeof deferred.resolveData === "function"
  5103. );
  5104. }
  5105. function isResponse(value: any): value is Response {
  5106. return (
  5107. value != null &&
  5108. typeof value.status === "number" &&
  5109. typeof value.statusText === "string" &&
  5110. typeof value.headers === "object" &&
  5111. typeof value.body !== "undefined"
  5112. );
  5113. }
  5114. function isRedirectResponse(result: any): result is Response {
  5115. if (!isResponse(result)) {
  5116. return false;
  5117. }
  5118. let status = result.status;
  5119. let location = result.headers.get("Location");
  5120. return status >= 300 && status <= 399 && location != null;
  5121. }
  5122. function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
  5123. return validRequestMethods.has(method.toLowerCase() as FormMethod);
  5124. }
  5125. function isMutationMethod(
  5126. method: string
  5127. ): method is MutationFormMethod | V7_MutationFormMethod {
  5128. return validMutationMethods.has(method.toLowerCase() as MutationFormMethod);
  5129. }
  5130. async function resolveNavigationDeferredResults(
  5131. matches: (AgnosticDataRouteMatch | null)[],
  5132. results: Record<string, DataResult>,
  5133. signal: AbortSignal,
  5134. currentMatches: AgnosticDataRouteMatch[],
  5135. currentLoaderData: RouteData
  5136. ) {
  5137. let entries = Object.entries(results);
  5138. for (let index = 0; index < entries.length; index++) {
  5139. let [routeId, result] = entries[index];
  5140. let match = matches.find((m) => m?.route.id === routeId);
  5141. // If we don't have a match, then we can have a deferred result to do
  5142. // anything with. This is for revalidating fetchers where the route was
  5143. // removed during HMR
  5144. if (!match) {
  5145. continue;
  5146. }
  5147. let currentMatch = currentMatches.find(
  5148. (m) => m.route.id === match!.route.id
  5149. );
  5150. let isRevalidatingLoader =
  5151. currentMatch != null &&
  5152. !isNewRouteInstance(currentMatch, match) &&
  5153. (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
  5154. if (isDeferredResult(result) && isRevalidatingLoader) {
  5155. // Note: we do not have to touch activeDeferreds here since we race them
  5156. // against the signal in resolveDeferredData and they'll get aborted
  5157. // there if needed
  5158. await resolveDeferredData(result, signal, false).then((result) => {
  5159. if (result) {
  5160. results[routeId] = result;
  5161. }
  5162. });
  5163. }
  5164. }
  5165. }
  5166. async function resolveFetcherDeferredResults(
  5167. matches: (AgnosticDataRouteMatch | null)[],
  5168. results: Record<string, DataResult>,
  5169. revalidatingFetchers: RevalidatingFetcher[]
  5170. ) {
  5171. for (let index = 0; index < revalidatingFetchers.length; index++) {
  5172. let { key, routeId, controller } = revalidatingFetchers[index];
  5173. let result = results[key];
  5174. let match = matches.find((m) => m?.route.id === routeId);
  5175. // If we don't have a match, then we can have a deferred result to do
  5176. // anything with. This is for revalidating fetchers where the route was
  5177. // removed during HMR
  5178. if (!match) {
  5179. continue;
  5180. }
  5181. if (isDeferredResult(result)) {
  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. invariant(
  5186. controller,
  5187. "Expected an AbortController for revalidating fetcher deferred result"
  5188. );
  5189. await resolveDeferredData(result, controller.signal, true).then(
  5190. (result) => {
  5191. if (result) {
  5192. results[key] = result;
  5193. }
  5194. }
  5195. );
  5196. }
  5197. }
  5198. }
  5199. async function resolveDeferredData(
  5200. result: DeferredResult,
  5201. signal: AbortSignal,
  5202. unwrap = false
  5203. ): Promise<SuccessResult | ErrorResult | undefined> {
  5204. let aborted = await result.deferredData.resolveData(signal);
  5205. if (aborted) {
  5206. return;
  5207. }
  5208. if (unwrap) {
  5209. try {
  5210. return {
  5211. type: ResultType.data,
  5212. data: result.deferredData.unwrappedData,
  5213. };
  5214. } catch (e) {
  5215. // Handle any TrackedPromise._error values encountered while unwrapping
  5216. return {
  5217. type: ResultType.error,
  5218. error: e,
  5219. };
  5220. }
  5221. }
  5222. return {
  5223. type: ResultType.data,
  5224. data: result.deferredData.data,
  5225. };
  5226. }
  5227. function hasNakedIndexQuery(search: string): boolean {
  5228. return new URLSearchParams(search).getAll("index").some((v) => v === "");
  5229. }
  5230. function getTargetMatch(
  5231. matches: AgnosticDataRouteMatch[],
  5232. location: Location | string
  5233. ) {
  5234. let search =
  5235. typeof location === "string" ? parsePath(location).search : location.search;
  5236. if (
  5237. matches[matches.length - 1].route.index &&
  5238. hasNakedIndexQuery(search || "")
  5239. ) {
  5240. // Return the leaf index route when index is present
  5241. return matches[matches.length - 1];
  5242. }
  5243. // Otherwise grab the deepest "path contributing" match (ignoring index and
  5244. // pathless layout routes)
  5245. let pathMatches = getPathContributingMatches(matches);
  5246. return pathMatches[pathMatches.length - 1];
  5247. }
  5248. function getSubmissionFromNavigation(
  5249. navigation: Navigation
  5250. ): Submission | undefined {
  5251. let { formMethod, formAction, formEncType, text, formData, json } =
  5252. navigation;
  5253. if (!formMethod || !formAction || !formEncType) {
  5254. return;
  5255. }
  5256. if (text != null) {
  5257. return {
  5258. formMethod,
  5259. formAction,
  5260. formEncType,
  5261. formData: undefined,
  5262. json: undefined,
  5263. text,
  5264. };
  5265. } else if (formData != null) {
  5266. return {
  5267. formMethod,
  5268. formAction,
  5269. formEncType,
  5270. formData,
  5271. json: undefined,
  5272. text: undefined,
  5273. };
  5274. } else if (json !== undefined) {
  5275. return {
  5276. formMethod,
  5277. formAction,
  5278. formEncType,
  5279. formData: undefined,
  5280. json,
  5281. text: undefined,
  5282. };
  5283. }
  5284. }
  5285. function getLoadingNavigation(
  5286. location: Location,
  5287. submission?: Submission
  5288. ): NavigationStates["Loading"] {
  5289. if (submission) {
  5290. let navigation: NavigationStates["Loading"] = {
  5291. state: "loading",
  5292. location,
  5293. formMethod: submission.formMethod,
  5294. formAction: submission.formAction,
  5295. formEncType: submission.formEncType,
  5296. formData: submission.formData,
  5297. json: submission.json,
  5298. text: submission.text,
  5299. };
  5300. return navigation;
  5301. } else {
  5302. let navigation: NavigationStates["Loading"] = {
  5303. state: "loading",
  5304. location,
  5305. formMethod: undefined,
  5306. formAction: undefined,
  5307. formEncType: undefined,
  5308. formData: undefined,
  5309. json: undefined,
  5310. text: undefined,
  5311. };
  5312. return navigation;
  5313. }
  5314. }
  5315. function getSubmittingNavigation(
  5316. location: Location,
  5317. submission: Submission
  5318. ): NavigationStates["Submitting"] {
  5319. let navigation: NavigationStates["Submitting"] = {
  5320. state: "submitting",
  5321. location,
  5322. formMethod: submission.formMethod,
  5323. formAction: submission.formAction,
  5324. formEncType: submission.formEncType,
  5325. formData: submission.formData,
  5326. json: submission.json,
  5327. text: submission.text,
  5328. };
  5329. return navigation;
  5330. }
  5331. function getLoadingFetcher(
  5332. submission?: Submission,
  5333. data?: Fetcher["data"]
  5334. ): FetcherStates["Loading"] {
  5335. if (submission) {
  5336. let fetcher: FetcherStates["Loading"] = {
  5337. state: "loading",
  5338. formMethod: submission.formMethod,
  5339. formAction: submission.formAction,
  5340. formEncType: submission.formEncType,
  5341. formData: submission.formData,
  5342. json: submission.json,
  5343. text: submission.text,
  5344. data,
  5345. };
  5346. return fetcher;
  5347. } else {
  5348. let fetcher: FetcherStates["Loading"] = {
  5349. state: "loading",
  5350. formMethod: undefined,
  5351. formAction: undefined,
  5352. formEncType: undefined,
  5353. formData: undefined,
  5354. json: undefined,
  5355. text: undefined,
  5356. data,
  5357. };
  5358. return fetcher;
  5359. }
  5360. }
  5361. function getSubmittingFetcher(
  5362. submission: Submission,
  5363. existingFetcher?: Fetcher
  5364. ): FetcherStates["Submitting"] {
  5365. let fetcher: FetcherStates["Submitting"] = {
  5366. state: "submitting",
  5367. formMethod: submission.formMethod,
  5368. formAction: submission.formAction,
  5369. formEncType: submission.formEncType,
  5370. formData: submission.formData,
  5371. json: submission.json,
  5372. text: submission.text,
  5373. data: existingFetcher ? existingFetcher.data : undefined,
  5374. };
  5375. return fetcher;
  5376. }
  5377. function getDoneFetcher(data: Fetcher["data"]): FetcherStates["Idle"] {
  5378. let fetcher: FetcherStates["Idle"] = {
  5379. state: "idle",
  5380. formMethod: undefined,
  5381. formAction: undefined,
  5382. formEncType: undefined,
  5383. formData: undefined,
  5384. json: undefined,
  5385. text: undefined,
  5386. data,
  5387. };
  5388. return fetcher;
  5389. }
  5390. function restoreAppliedTransitions(
  5391. _window: Window,
  5392. transitions: Map<string, Set<string>>
  5393. ) {
  5394. try {
  5395. let sessionPositions = _window.sessionStorage.getItem(
  5396. TRANSITIONS_STORAGE_KEY
  5397. );
  5398. if (sessionPositions) {
  5399. let json = JSON.parse(sessionPositions);
  5400. for (let [k, v] of Object.entries(json || {})) {
  5401. if (v && Array.isArray(v)) {
  5402. transitions.set(k, new Set(v || []));
  5403. }
  5404. }
  5405. }
  5406. } catch (e) {
  5407. // no-op, use default empty object
  5408. }
  5409. }
  5410. function persistAppliedTransitions(
  5411. _window: Window,
  5412. transitions: Map<string, Set<string>>
  5413. ) {
  5414. if (transitions.size > 0) {
  5415. let json: Record<string, string[]> = {};
  5416. for (let [k, v] of transitions) {
  5417. json[k] = [...v];
  5418. }
  5419. try {
  5420. _window.sessionStorage.setItem(
  5421. TRANSITIONS_STORAGE_KEY,
  5422. JSON.stringify(json)
  5423. );
  5424. } catch (error) {
  5425. warning(
  5426. false,
  5427. `Failed to save applied view transitions in sessionStorage (${error}).`
  5428. );
  5429. }
  5430. }
  5431. }
  5432. //#endregion