12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061 |
- /**
- * @remix-run/router v1.3.2
- *
- * Copyright (c) Remix Software Inc.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.md file in the root directory of this source tree.
- *
- * @license MIT
- */
- 'use strict';
- Object.defineProperty(exports, '__esModule', { value: true });
- function _extends() {
- _extends = Object.assign ? Object.assign.bind() : function (target) {
- for (var i = 1; i < arguments.length; i++) {
- var source = arguments[i];
- for (var key in source) {
- if (Object.prototype.hasOwnProperty.call(source, key)) {
- target[key] = source[key];
- }
- }
- }
- return target;
- };
- return _extends.apply(this, arguments);
- }
- ////////////////////////////////////////////////////////////////////////////////
- //#region Types and Constants
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * Actions represent the type of change to a location value.
- */
- exports.Action = void 0;
- /**
- * The pathname, search, and hash values of a URL.
- */
- (function (Action) {
- Action["Pop"] = "POP";
- Action["Push"] = "PUSH";
- Action["Replace"] = "REPLACE";
- })(exports.Action || (exports.Action = {}));
- const PopStateEventType = "popstate"; //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region Memory History
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * A user-supplied object that describes a location. Used when providing
- * entries to `createMemoryHistory` via its `initialEntries` option.
- */
- /**
- * Memory history stores the current location in memory. It is designed for use
- * in stateful non-browser environments like tests and React Native.
- */
- function createMemoryHistory(options) {
- if (options === void 0) {
- options = {};
- }
- let {
- initialEntries = ["/"],
- initialIndex,
- v5Compat = false
- } = options;
- let entries; // Declare so we can access from createMemoryLocation
- entries = initialEntries.map((entry, index) => createMemoryLocation(entry, typeof entry === "string" ? null : entry.state, index === 0 ? "default" : undefined));
- let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex);
- let action = exports.Action.Pop;
- let listener = null;
- function clampIndex(n) {
- return Math.min(Math.max(n, 0), entries.length - 1);
- }
- function getCurrentLocation() {
- return entries[index];
- }
- function createMemoryLocation(to, state, key) {
- if (state === void 0) {
- state = null;
- }
- let location = createLocation(entries ? getCurrentLocation().pathname : "/", to, state, key);
- warning$1(location.pathname.charAt(0) === "/", "relative pathnames are not supported in memory history: " + JSON.stringify(to));
- return location;
- }
- function createHref(to) {
- return typeof to === "string" ? to : createPath(to);
- }
- let history = {
- get index() {
- return index;
- },
- get action() {
- return action;
- },
- get location() {
- return getCurrentLocation();
- },
- createHref,
- createURL(to) {
- return new URL(createHref(to), "http://localhost");
- },
- encodeLocation(to) {
- let path = typeof to === "string" ? parsePath(to) : to;
- return {
- pathname: path.pathname || "",
- search: path.search || "",
- hash: path.hash || ""
- };
- },
- push(to, state) {
- action = exports.Action.Push;
- let nextLocation = createMemoryLocation(to, state);
- index += 1;
- entries.splice(index, entries.length, nextLocation);
- if (v5Compat && listener) {
- listener({
- action,
- location: nextLocation,
- delta: 1
- });
- }
- },
- replace(to, state) {
- action = exports.Action.Replace;
- let nextLocation = createMemoryLocation(to, state);
- entries[index] = nextLocation;
- if (v5Compat && listener) {
- listener({
- action,
- location: nextLocation,
- delta: 0
- });
- }
- },
- go(delta) {
- action = exports.Action.Pop;
- let nextIndex = clampIndex(index + delta);
- let nextLocation = entries[nextIndex];
- index = nextIndex;
- if (listener) {
- listener({
- action,
- location: nextLocation,
- delta
- });
- }
- },
- listen(fn) {
- listener = fn;
- return () => {
- listener = null;
- };
- }
- };
- return history;
- } //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region Browser History
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * A browser history stores the current location in regular URLs in a web
- * browser environment. This is the standard for most web apps and provides the
- * cleanest URLs the browser's address bar.
- *
- * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#browserhistory
- */
- /**
- * Browser history stores the location in regular URLs. This is the standard for
- * most web apps, but it requires some configuration on the server to ensure you
- * serve the same app at multiple URLs.
- *
- * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory
- */
- function createBrowserHistory(options) {
- if (options === void 0) {
- options = {};
- }
- function createBrowserLocation(window, globalHistory) {
- let {
- pathname,
- search,
- hash
- } = window.location;
- return createLocation("", {
- pathname,
- search,
- hash
- }, // state defaults to `null` because `window.history.state` does
- globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");
- }
- function createBrowserHref(window, to) {
- return typeof to === "string" ? to : createPath(to);
- }
- return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);
- } //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region Hash History
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * A hash history stores the current location in the fragment identifier portion
- * of the URL in a web browser environment.
- *
- * This is ideal for apps that do not control the server for some reason
- * (because the fragment identifier is never sent to the server), including some
- * shared hosting environments that do not provide fine-grained controls over
- * which pages are served at which URLs.
- *
- * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#hashhistory
- */
- /**
- * Hash history stores the location in window.location.hash. This makes it ideal
- * for situations where you don't want to send the location to the server for
- * some reason, either because you do cannot configure it or the URL space is
- * reserved for something else.
- *
- * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory
- */
- function createHashHistory(options) {
- if (options === void 0) {
- options = {};
- }
- function createHashLocation(window, globalHistory) {
- let {
- pathname = "/",
- search = "",
- hash = ""
- } = parsePath(window.location.hash.substr(1));
- return createLocation("", {
- pathname,
- search,
- hash
- }, // state defaults to `null` because `window.history.state` does
- globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");
- }
- function createHashHref(window, to) {
- let base = window.document.querySelector("base");
- let href = "";
- if (base && base.getAttribute("href")) {
- let url = window.location.href;
- let hashIndex = url.indexOf("#");
- href = hashIndex === -1 ? url : url.slice(0, hashIndex);
- }
- return href + "#" + (typeof to === "string" ? to : createPath(to));
- }
- function validateHashLocation(location, to) {
- warning$1(location.pathname.charAt(0) === "/", "relative pathnames are not supported in hash history.push(" + JSON.stringify(to) + ")");
- }
- return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options);
- } //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region UTILS
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * @private
- */
- function invariant(value, message) {
- if (value === false || value === null || typeof value === "undefined") {
- throw new Error(message);
- }
- }
- function warning$1(cond, message) {
- if (!cond) {
- // eslint-disable-next-line no-console
- if (typeof console !== "undefined") console.warn(message);
- try {
- // Welcome to debugging history!
- //
- // This error is thrown as a convenience so you can more easily
- // find the source for a warning that appears in the console by
- // enabling "pause on exceptions" in your JavaScript debugger.
- throw new Error(message); // eslint-disable-next-line no-empty
- } catch (e) {}
- }
- }
- function createKey() {
- return Math.random().toString(36).substr(2, 8);
- }
- /**
- * For browser-based histories, we combine the state and key into an object
- */
- function getHistoryState(location, index) {
- return {
- usr: location.state,
- key: location.key,
- idx: index
- };
- }
- /**
- * Creates a Location object with a unique key from the given Path
- */
- function createLocation(current, to, state, key) {
- if (state === void 0) {
- state = null;
- }
- let location = _extends({
- pathname: typeof current === "string" ? current : current.pathname,
- search: "",
- hash: ""
- }, typeof to === "string" ? parsePath(to) : to, {
- state,
- // TODO: This could be cleaned up. push/replace should probably just take
- // full Locations now and avoid the need to run through this flow at all
- // But that's a pretty big refactor to the current test suite so going to
- // keep as is for the time being and just let any incoming keys take precedence
- key: to && to.key || key || createKey()
- });
- return location;
- }
- /**
- * Creates a string URL path from the given pathname, search, and hash components.
- */
- function createPath(_ref) {
- let {
- pathname = "/",
- search = "",
- hash = ""
- } = _ref;
- if (search && search !== "?") pathname += search.charAt(0) === "?" ? search : "?" + search;
- if (hash && hash !== "#") pathname += hash.charAt(0) === "#" ? hash : "#" + hash;
- return pathname;
- }
- /**
- * Parses a string URL path into its separate pathname, search, and hash components.
- */
- function parsePath(path) {
- let parsedPath = {};
- if (path) {
- let hashIndex = path.indexOf("#");
- if (hashIndex >= 0) {
- parsedPath.hash = path.substr(hashIndex);
- path = path.substr(0, hashIndex);
- }
- let searchIndex = path.indexOf("?");
- if (searchIndex >= 0) {
- parsedPath.search = path.substr(searchIndex);
- path = path.substr(0, searchIndex);
- }
- if (path) {
- parsedPath.pathname = path;
- }
- }
- return parsedPath;
- }
- function getUrlBasedHistory(getLocation, createHref, validateLocation, options) {
- if (options === void 0) {
- options = {};
- }
- let {
- window = document.defaultView,
- v5Compat = false
- } = options;
- let globalHistory = window.history;
- let action = exports.Action.Pop;
- let listener = null;
- let index = getIndex(); // Index should only be null when we initialize. If not, it's because the
- // user called history.pushState or history.replaceState directly, in which
- // case we should log a warning as it will result in bugs.
- if (index == null) {
- index = 0;
- globalHistory.replaceState(_extends({}, globalHistory.state, {
- idx: index
- }), "");
- }
- function getIndex() {
- let state = globalHistory.state || {
- idx: null
- };
- return state.idx;
- }
- function handlePop() {
- action = exports.Action.Pop;
- let nextIndex = getIndex();
- let delta = nextIndex == null ? null : nextIndex - index;
- index = nextIndex;
- if (listener) {
- listener({
- action,
- location: history.location,
- delta
- });
- }
- }
- function push(to, state) {
- action = exports.Action.Push;
- let location = createLocation(history.location, to, state);
- if (validateLocation) validateLocation(location, to);
- index = getIndex() + 1;
- let historyState = getHistoryState(location, index);
- let url = history.createHref(location); // try...catch because iOS limits us to 100 pushState calls :/
- try {
- globalHistory.pushState(historyState, "", url);
- } catch (error) {
- // They are going to lose state here, but there is no real
- // way to warn them about it since the page will refresh...
- window.location.assign(url);
- }
- if (v5Compat && listener) {
- listener({
- action,
- location: history.location,
- delta: 1
- });
- }
- }
- function replace(to, state) {
- action = exports.Action.Replace;
- let location = createLocation(history.location, to, state);
- if (validateLocation) validateLocation(location, to);
- index = getIndex();
- let historyState = getHistoryState(location, index);
- let url = history.createHref(location);
- globalHistory.replaceState(historyState, "", url);
- if (v5Compat && listener) {
- listener({
- action,
- location: history.location,
- delta: 0
- });
- }
- }
- function createURL(to) {
- // window.location.origin is "null" (the literal string value) in Firefox
- // under certain conditions, notably when serving from a local HTML file
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
- let base = window.location.origin !== "null" ? window.location.origin : window.location.href;
- let href = typeof to === "string" ? to : createPath(to);
- invariant(base, "No window.location.(origin|href) available to create URL for href: " + href);
- return new URL(href, base);
- }
- let history = {
- get action() {
- return action;
- },
- get location() {
- return getLocation(window, globalHistory);
- },
- listen(fn) {
- if (listener) {
- throw new Error("A history only accepts one active listener");
- }
- window.addEventListener(PopStateEventType, handlePop);
- listener = fn;
- return () => {
- window.removeEventListener(PopStateEventType, handlePop);
- listener = null;
- };
- },
- createHref(to) {
- return createHref(window, to);
- },
- createURL,
- encodeLocation(to) {
- // Encode a Location the same way window.location would
- let url = createURL(to);
- return {
- pathname: url.pathname,
- search: url.search,
- hash: url.hash
- };
- },
- push,
- replace,
- go(n) {
- return globalHistory.go(n);
- }
- };
- return history;
- } //#endregion
- /**
- * Map of routeId -> data returned from a loader/action/error
- */
- let ResultType;
- /**
- * Successful result from a loader or action
- */
- (function (ResultType) {
- ResultType["data"] = "data";
- ResultType["deferred"] = "deferred";
- ResultType["redirect"] = "redirect";
- ResultType["error"] = "error";
- })(ResultType || (ResultType = {}));
- function isIndexRoute(route) {
- return route.index === true;
- } // Walk the route tree generating unique IDs where necessary so we are working
- // solely with AgnosticDataRouteObject's within the Router
- function convertRoutesToDataRoutes(routes, parentPath, allIds) {
- if (parentPath === void 0) {
- parentPath = [];
- }
- if (allIds === void 0) {
- allIds = new Set();
- }
- return routes.map((route, index) => {
- let treePath = [...parentPath, index];
- let id = typeof route.id === "string" ? route.id : treePath.join("-");
- invariant(route.index !== true || !route.children, "Cannot specify children on an index route");
- invariant(!allIds.has(id), "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages");
- allIds.add(id);
- if (isIndexRoute(route)) {
- let indexRoute = _extends({}, route, {
- id
- });
- return indexRoute;
- } else {
- let pathOrLayoutRoute = _extends({}, route, {
- id,
- children: route.children ? convertRoutesToDataRoutes(route.children, treePath, allIds) : undefined
- });
- return pathOrLayoutRoute;
- }
- });
- }
- /**
- * Matches the given routes to a location and returns the match data.
- *
- * @see https://reactrouter.com/utils/match-routes
- */
- function matchRoutes(routes, locationArg, basename) {
- if (basename === void 0) {
- basename = "/";
- }
- let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
- let pathname = stripBasename(location.pathname || "/", basename);
- if (pathname == null) {
- return null;
- }
- let branches = flattenRoutes(routes);
- rankRouteBranches(branches);
- let matches = null;
- for (let i = 0; matches == null && i < branches.length; ++i) {
- matches = matchRouteBranch(branches[i], // Incoming pathnames are generally encoded from either window.location
- // or from router.navigate, but we want to match against the unencoded
- // paths in the route definitions. Memory router locations won't be
- // encoded here but there also shouldn't be anything to decode so this
- // should be a safe operation. This avoids needing matchRoutes to be
- // history-aware.
- safelyDecodeURI(pathname));
- }
- return matches;
- }
- function flattenRoutes(routes, branches, parentsMeta, parentPath) {
- if (branches === void 0) {
- branches = [];
- }
- if (parentsMeta === void 0) {
- parentsMeta = [];
- }
- if (parentPath === void 0) {
- parentPath = "";
- }
- let flattenRoute = (route, index, relativePath) => {
- let meta = {
- relativePath: relativePath === undefined ? route.path || "" : relativePath,
- caseSensitive: route.caseSensitive === true,
- childrenIndex: index,
- route
- };
- if (meta.relativePath.startsWith("/")) {
- invariant(meta.relativePath.startsWith(parentPath), "Absolute route path \"" + meta.relativePath + "\" nested under path " + ("\"" + parentPath + "\" is not valid. An absolute child route path ") + "must start with the combined path of all its parent routes.");
- meta.relativePath = meta.relativePath.slice(parentPath.length);
- }
- let path = joinPaths([parentPath, meta.relativePath]);
- let routesMeta = parentsMeta.concat(meta); // Add the children before adding this route to the array so we traverse the
- // route tree depth-first and child routes appear before their parents in
- // the "flattened" version.
- if (route.children && route.children.length > 0) {
- invariant( // Our types know better, but runtime JS may not!
- // @ts-expect-error
- route.index !== true, "Index routes must not have child routes. Please remove " + ("all child routes from route path \"" + path + "\"."));
- flattenRoutes(route.children, branches, routesMeta, path);
- } // Routes without a path shouldn't ever match by themselves unless they are
- // index routes, so don't add them to the list of possible branches.
- if (route.path == null && !route.index) {
- return;
- }
- branches.push({
- path,
- score: computeScore(path, route.index),
- routesMeta
- });
- };
- routes.forEach((route, index) => {
- var _route$path;
- // coarse-grain check for optional params
- if (route.path === "" || !((_route$path = route.path) != null && _route$path.includes("?"))) {
- flattenRoute(route, index);
- } else {
- for (let exploded of explodeOptionalSegments(route.path)) {
- flattenRoute(route, index, exploded);
- }
- }
- });
- return branches;
- }
- /**
- * Computes all combinations of optional path segments for a given path,
- * excluding combinations that are ambiguous and of lower priority.
- *
- * For example, `/one/:two?/three/:four?/:five?` explodes to:
- * - `/one/three`
- * - `/one/:two/three`
- * - `/one/three/:four`
- * - `/one/three/:five`
- * - `/one/:two/three/:four`
- * - `/one/:two/three/:five`
- * - `/one/three/:four/:five`
- * - `/one/:two/three/:four/:five`
- */
- function explodeOptionalSegments(path) {
- let segments = path.split("/");
- if (segments.length === 0) return [];
- let [first, ...rest] = segments; // Optional path segments are denoted by a trailing `?`
- let isOptional = first.endsWith("?"); // Compute the corresponding required segment: `foo?` -> `foo`
- let required = first.replace(/\?$/, "");
- if (rest.length === 0) {
- // Intepret empty string as omitting an optional segment
- // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
- return isOptional ? [required, ""] : [required];
- }
- let restExploded = explodeOptionalSegments(rest.join("/"));
- let result = []; // All child paths with the prefix. Do this for all children before the
- // optional version for all children so we get consistent ordering where the
- // parent optional aspect is preferred as required. Otherwise, we can get
- // child sections interspersed where deeper optional segments are higher than
- // parent optional segments, where for example, /:two would explodes _earlier_
- // then /:one. By always including the parent as required _for all children_
- // first, we avoid this issue
- result.push(...restExploded.map(subpath => subpath === "" ? required : [required, subpath].join("/"))); // Then if this is an optional value, add all child versions without
- if (isOptional) {
- result.push(...restExploded);
- } // for absolute paths, ensure `/` instead of empty segment
- return result.map(exploded => path.startsWith("/") && exploded === "" ? "/" : exploded);
- }
- function rankRouteBranches(branches) {
- branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
- : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));
- }
- const paramRe = /^:\w+$/;
- const dynamicSegmentValue = 3;
- const indexRouteValue = 2;
- const emptySegmentValue = 1;
- const staticSegmentValue = 10;
- const splatPenalty = -2;
- const isSplat = s => s === "*";
- function computeScore(path, index) {
- let segments = path.split("/");
- let initialScore = segments.length;
- if (segments.some(isSplat)) {
- initialScore += splatPenalty;
- }
- if (index) {
- initialScore += indexRouteValue;
- }
- return segments.filter(s => !isSplat(s)).reduce((score, segment) => score + (paramRe.test(segment) ? dynamicSegmentValue : segment === "" ? emptySegmentValue : staticSegmentValue), initialScore);
- }
- function compareIndexes(a, b) {
- let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
- return siblings ? // If two routes are siblings, we should try to match the earlier sibling
- // first. This allows people to have fine-grained control over the matching
- // behavior by simply putting routes with identical paths in the order they
- // want them tried.
- a[a.length - 1] - b[b.length - 1] : // Otherwise, it doesn't really make sense to rank non-siblings by index,
- // so they sort equally.
- 0;
- }
- function matchRouteBranch(branch, pathname) {
- let {
- routesMeta
- } = branch;
- let matchedParams = {};
- let matchedPathname = "/";
- let matches = [];
- for (let i = 0; i < routesMeta.length; ++i) {
- let meta = routesMeta[i];
- let end = i === routesMeta.length - 1;
- let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
- let match = matchPath({
- path: meta.relativePath,
- caseSensitive: meta.caseSensitive,
- end
- }, remainingPathname);
- if (!match) return null;
- Object.assign(matchedParams, match.params);
- let route = meta.route;
- matches.push({
- // TODO: Can this as be avoided?
- params: matchedParams,
- pathname: joinPaths([matchedPathname, match.pathname]),
- pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),
- route
- });
- if (match.pathnameBase !== "/") {
- matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
- }
- }
- return matches;
- }
- /**
- * Returns a path with params interpolated.
- *
- * @see https://reactrouter.com/utils/generate-path
- */
- function generatePath(originalPath, params) {
- if (params === void 0) {
- params = {};
- }
- let path = originalPath;
- if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
- warning(false, "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
- path = path.replace(/\*$/, "/*");
- }
- return path.replace(/^:(\w+)(\??)/g, (_, key, optional) => {
- let param = params[key];
- if (optional === "?") {
- return param == null ? "" : param;
- }
- if (param == null) {
- invariant(false, "Missing \":" + key + "\" param");
- }
- return param;
- }).replace(/\/:(\w+)(\??)/g, (_, key, optional) => {
- let param = params[key];
- if (optional === "?") {
- return param == null ? "" : "/" + param;
- }
- if (param == null) {
- invariant(false, "Missing \":" + key + "\" param");
- }
- return "/" + param;
- }) // Remove any optional markers from optional static segments
- .replace(/\?/g, "").replace(/(\/?)\*/, (_, prefix, __, str) => {
- const star = "*";
- if (params[star] == null) {
- // If no splat was provided, trim the trailing slash _unless_ it's
- // the entire path
- return str === "/*" ? "/" : "";
- } // Apply the splat
- return "" + prefix + params[star];
- });
- }
- /**
- * A PathPattern is used to match on some portion of a URL pathname.
- */
- /**
- * Performs pattern matching on a URL pathname and returns information about
- * the match.
- *
- * @see https://reactrouter.com/utils/match-path
- */
- function matchPath(pattern, pathname) {
- if (typeof pattern === "string") {
- pattern = {
- path: pattern,
- caseSensitive: false,
- end: true
- };
- }
- let [matcher, paramNames] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
- let match = pathname.match(matcher);
- if (!match) return null;
- let matchedPathname = match[0];
- let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
- let captureGroups = match.slice(1);
- let params = paramNames.reduce((memo, paramName, index) => {
- // We need to compute the pathnameBase here using the raw splat value
- // instead of using params["*"] later because it will be decoded then
- if (paramName === "*") {
- let splatValue = captureGroups[index] || "";
- pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
- }
- memo[paramName] = safelyDecodeURIComponent(captureGroups[index] || "", paramName);
- return memo;
- }, {});
- return {
- params,
- pathname: matchedPathname,
- pathnameBase,
- pattern
- };
- }
- function compilePath(path, caseSensitive, end) {
- if (caseSensitive === void 0) {
- caseSensitive = false;
- }
- if (end === void 0) {
- end = true;
- }
- warning(path === "*" || !path.endsWith("*") || path.endsWith("/*"), "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
- let paramNames = [];
- let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
- .replace(/^\/*/, "/") // Make sure it has a leading /
- .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
- .replace(/\/:(\w+)/g, (_, paramName) => {
- paramNames.push(paramName);
- return "/([^\\/]+)";
- });
- if (path.endsWith("*")) {
- paramNames.push("*");
- regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest
- : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
- } else if (end) {
- // When matching to the end, ignore trailing slashes
- regexpSource += "\\/*$";
- } else if (path !== "" && path !== "/") {
- // If our path is non-empty and contains anything beyond an initial slash,
- // then we have _some_ form of path in our regex so we should expect to
- // match only if we find the end of this path segment. Look for an optional
- // non-captured trailing slash (to match a portion of the URL) or the end
- // of the path (if we've matched to the end). We used to do this with a
- // word boundary but that gives false positives on routes like
- // /user-preferences since `-` counts as a word boundary.
- regexpSource += "(?:(?=\\/|$))";
- } else ;
- let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
- return [matcher, paramNames];
- }
- function safelyDecodeURI(value) {
- try {
- return decodeURI(value);
- } catch (error) {
- warning(false, "The URL path \"" + value + "\" could not be decoded because it is is a " + "malformed URL segment. This is probably due to a bad percent " + ("encoding (" + error + ")."));
- return value;
- }
- }
- function safelyDecodeURIComponent(value, paramName) {
- try {
- return decodeURIComponent(value);
- } catch (error) {
- warning(false, "The value for the URL param \"" + paramName + "\" will not be decoded because" + (" the string \"" + value + "\" is a malformed URL segment. This is probably") + (" due to a bad percent encoding (" + error + ")."));
- return value;
- }
- }
- /**
- * @private
- */
- function stripBasename(pathname, basename) {
- if (basename === "/") return pathname;
- if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
- return null;
- } // We want to leave trailing slash behavior in the user's control, so if they
- // specify a basename with a trailing slash, we should support it
- let startIndex = basename.endsWith("/") ? basename.length - 1 : basename.length;
- let nextChar = pathname.charAt(startIndex);
- if (nextChar && nextChar !== "/") {
- // pathname does not start with basename/
- return null;
- }
- return pathname.slice(startIndex) || "/";
- }
- /**
- * @private
- */
- function warning(cond, message) {
- if (!cond) {
- // eslint-disable-next-line no-console
- if (typeof console !== "undefined") console.warn(message);
- try {
- // Welcome to debugging @remix-run/router!
- //
- // This error is thrown as a convenience so you can more easily
- // find the source for a warning that appears in the console by
- // enabling "pause on exceptions" in your JavaScript debugger.
- throw new Error(message); // eslint-disable-next-line no-empty
- } catch (e) {}
- }
- }
- /**
- * Returns a resolved path object relative to the given pathname.
- *
- * @see https://reactrouter.com/utils/resolve-path
- */
- function resolvePath(to, fromPathname) {
- if (fromPathname === void 0) {
- fromPathname = "/";
- }
- let {
- pathname: toPathname,
- search = "",
- hash = ""
- } = typeof to === "string" ? parsePath(to) : to;
- let pathname = toPathname ? toPathname.startsWith("/") ? toPathname : resolvePathname(toPathname, fromPathname) : fromPathname;
- return {
- pathname,
- search: normalizeSearch(search),
- hash: normalizeHash(hash)
- };
- }
- function resolvePathname(relativePath, fromPathname) {
- let segments = fromPathname.replace(/\/+$/, "").split("/");
- let relativeSegments = relativePath.split("/");
- relativeSegments.forEach(segment => {
- if (segment === "..") {
- // Keep the root "" segment so the pathname starts at /
- if (segments.length > 1) segments.pop();
- } else if (segment !== ".") {
- segments.push(segment);
- }
- });
- return segments.length > 1 ? segments.join("/") : "/";
- }
- function getInvalidPathError(char, field, dest, path) {
- return "Cannot include a '" + char + "' character in a manually specified " + ("`to." + field + "` field [" + JSON.stringify(path) + "]. Please separate it out to the ") + ("`to." + dest + "` field. Alternatively you may provide the full path as ") + "a string in <Link to=\"...\"> and the router will parse it for you.";
- }
- /**
- * @private
- *
- * When processing relative navigation we want to ignore ancestor routes that
- * do not contribute to the path, such that index/pathless layout routes don't
- * interfere.
- *
- * For example, when moving a route element into an index route and/or a
- * pathless layout route, relative link behavior contained within should stay
- * the same. Both of the following examples should link back to the root:
- *
- * <Route path="/">
- * <Route path="accounts" element={<Link to=".."}>
- * </Route>
- *
- * <Route path="/">
- * <Route path="accounts">
- * <Route element={<AccountsLayout />}> // <-- Does not contribute
- * <Route index element={<Link to=".."} /> // <-- Does not contribute
- * </Route
- * </Route>
- * </Route>
- */
- function getPathContributingMatches(matches) {
- return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
- }
- /**
- * @private
- */
- function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {
- if (isPathRelative === void 0) {
- isPathRelative = false;
- }
- let to;
- if (typeof toArg === "string") {
- to = parsePath(toArg);
- } else {
- to = _extends({}, toArg);
- invariant(!to.pathname || !to.pathname.includes("?"), getInvalidPathError("?", "pathname", "search", to));
- invariant(!to.pathname || !to.pathname.includes("#"), getInvalidPathError("#", "pathname", "hash", to));
- invariant(!to.search || !to.search.includes("#"), getInvalidPathError("#", "search", "hash", to));
- }
- let isEmptyPath = toArg === "" || to.pathname === "";
- let toPathname = isEmptyPath ? "/" : to.pathname;
- let from; // Routing is relative to the current pathname if explicitly requested.
- //
- // If a pathname is explicitly provided in `to`, it should be relative to the
- // route context. This is explained in `Note on `<Link to>` values` in our
- // migration guide from v5 as a means of disambiguation between `to` values
- // that begin with `/` and those that do not. However, this is problematic for
- // `to` values that do not provide a pathname. `to` can simply be a search or
- // hash string, in which case we should assume that the navigation is relative
- // to the current location's pathname and *not* the route pathname.
- if (isPathRelative || toPathname == null) {
- from = locationPathname;
- } else {
- let routePathnameIndex = routePathnames.length - 1;
- if (toPathname.startsWith("..")) {
- let toSegments = toPathname.split("/"); // Each leading .. segment means "go up one route" instead of "go up one
- // URL segment". This is a key difference from how <a href> works and a
- // major reason we call this a "to" value instead of a "href".
- while (toSegments[0] === "..") {
- toSegments.shift();
- routePathnameIndex -= 1;
- }
- to.pathname = toSegments.join("/");
- } // If there are more ".." segments than parent routes, resolve relative to
- // the root / URL.
- from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
- }
- let path = resolvePath(to, from); // Ensure the pathname has a trailing slash if the original "to" had one
- let hasExplicitTrailingSlash = toPathname && toPathname !== "/" && toPathname.endsWith("/"); // Or if this was a link to the current path which has a trailing slash
- let hasCurrentTrailingSlash = (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
- if (!path.pathname.endsWith("/") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) {
- path.pathname += "/";
- }
- return path;
- }
- /**
- * @private
- */
- function getToPathname(to) {
- // Empty strings should be treated the same as / paths
- return to === "" || to.pathname === "" ? "/" : typeof to === "string" ? parsePath(to).pathname : to.pathname;
- }
- /**
- * @private
- */
- const joinPaths = paths => paths.join("/").replace(/\/\/+/g, "/");
- /**
- * @private
- */
- const normalizePathname = pathname => pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
- /**
- * @private
- */
- const normalizeSearch = search => !search || search === "?" ? "" : search.startsWith("?") ? search : "?" + search;
- /**
- * @private
- */
- const normalizeHash = hash => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
- /**
- * This is a shortcut for creating `application/json` responses. Converts `data`
- * to JSON and sets the `Content-Type` header.
- */
- const json = function json(data, init) {
- if (init === void 0) {
- init = {};
- }
- let responseInit = typeof init === "number" ? {
- status: init
- } : init;
- let headers = new Headers(responseInit.headers);
- if (!headers.has("Content-Type")) {
- headers.set("Content-Type", "application/json; charset=utf-8");
- }
- return new Response(JSON.stringify(data), _extends({}, responseInit, {
- headers
- }));
- };
- class AbortedDeferredError extends Error {}
- class DeferredData {
- constructor(data, responseInit) {
- this.pendingKeysSet = new Set();
- this.subscribers = new Set();
- this.deferredKeys = [];
- invariant(data && typeof data === "object" && !Array.isArray(data), "defer() only accepts plain objects"); // Set up an AbortController + Promise we can race against to exit early
- // cancellation
- let reject;
- this.abortPromise = new Promise((_, r) => reject = r);
- this.controller = new AbortController();
- let onAbort = () => reject(new AbortedDeferredError("Deferred data aborted"));
- this.unlistenAbortSignal = () => this.controller.signal.removeEventListener("abort", onAbort);
- this.controller.signal.addEventListener("abort", onAbort);
- this.data = Object.entries(data).reduce((acc, _ref) => {
- let [key, value] = _ref;
- return Object.assign(acc, {
- [key]: this.trackPromise(key, value)
- });
- }, {});
- if (this.done) {
- // All incoming values were resolved
- this.unlistenAbortSignal();
- }
- this.init = responseInit;
- }
- trackPromise(key, value) {
- if (!(value instanceof Promise)) {
- return value;
- }
- this.deferredKeys.push(key);
- this.pendingKeysSet.add(key); // We store a little wrapper promise that will be extended with
- // _data/_error props upon resolve/reject
- let promise = Promise.race([value, this.abortPromise]).then(data => this.onSettle(promise, key, null, data), error => this.onSettle(promise, key, error)); // Register rejection listeners to avoid uncaught promise rejections on
- // errors or aborted deferred values
- promise.catch(() => {});
- Object.defineProperty(promise, "_tracked", {
- get: () => true
- });
- return promise;
- }
- onSettle(promise, key, error, data) {
- if (this.controller.signal.aborted && error instanceof AbortedDeferredError) {
- this.unlistenAbortSignal();
- Object.defineProperty(promise, "_error", {
- get: () => error
- });
- return Promise.reject(error);
- }
- this.pendingKeysSet.delete(key);
- if (this.done) {
- // Nothing left to abort!
- this.unlistenAbortSignal();
- }
- if (error) {
- Object.defineProperty(promise, "_error", {
- get: () => error
- });
- this.emit(false, key);
- return Promise.reject(error);
- }
- Object.defineProperty(promise, "_data", {
- get: () => data
- });
- this.emit(false, key);
- return data;
- }
- emit(aborted, settledKey) {
- this.subscribers.forEach(subscriber => subscriber(aborted, settledKey));
- }
- subscribe(fn) {
- this.subscribers.add(fn);
- return () => this.subscribers.delete(fn);
- }
- cancel() {
- this.controller.abort();
- this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));
- this.emit(true);
- }
- async resolveData(signal) {
- let aborted = false;
- if (!this.done) {
- let onAbort = () => this.cancel();
- signal.addEventListener("abort", onAbort);
- aborted = await new Promise(resolve => {
- this.subscribe(aborted => {
- signal.removeEventListener("abort", onAbort);
- if (aborted || this.done) {
- resolve(aborted);
- }
- });
- });
- }
- return aborted;
- }
- get done() {
- return this.pendingKeysSet.size === 0;
- }
- get unwrappedData() {
- invariant(this.data !== null && this.done, "Can only unwrap data on initialized and settled deferreds");
- return Object.entries(this.data).reduce((acc, _ref2) => {
- let [key, value] = _ref2;
- return Object.assign(acc, {
- [key]: unwrapTrackedPromise(value)
- });
- }, {});
- }
- get pendingKeys() {
- return Array.from(this.pendingKeysSet);
- }
- }
- function isTrackedPromise(value) {
- return value instanceof Promise && value._tracked === true;
- }
- function unwrapTrackedPromise(value) {
- if (!isTrackedPromise(value)) {
- return value;
- }
- if (value._error) {
- throw value._error;
- }
- return value._data;
- }
- const defer = function defer(data, init) {
- if (init === void 0) {
- init = {};
- }
- let responseInit = typeof init === "number" ? {
- status: init
- } : init;
- return new DeferredData(data, responseInit);
- };
- /**
- * A redirect response. Sets the status code and the `Location` header.
- * Defaults to "302 Found".
- */
- const redirect = function redirect(url, init) {
- if (init === void 0) {
- init = 302;
- }
- let responseInit = init;
- if (typeof responseInit === "number") {
- responseInit = {
- status: responseInit
- };
- } else if (typeof responseInit.status === "undefined") {
- responseInit.status = 302;
- }
- let headers = new Headers(responseInit.headers);
- headers.set("Location", url);
- return new Response(null, _extends({}, responseInit, {
- headers
- }));
- };
- /**
- * @private
- * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
- */
- class ErrorResponse {
- constructor(status, statusText, data, internal) {
- if (internal === void 0) {
- internal = false;
- }
- this.status = status;
- this.statusText = statusText || "";
- this.internal = internal;
- if (data instanceof Error) {
- this.data = data.toString();
- this.error = data;
- } else {
- this.data = data;
- }
- }
- }
- /**
- * Check if the given error is an ErrorResponse generated from a 4xx/5xx
- * Response thrown from an action/loader
- */
- function isRouteErrorResponse(error) {
- return error != null && typeof error.status === "number" && typeof error.statusText === "string" && typeof error.internal === "boolean" && "data" in error;
- }
- //#region Types and Constants
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * A Router instance manages all navigation and data loading/mutations
- */
- const validMutationMethodsArr = ["post", "put", "patch", "delete"];
- const validMutationMethods = new Set(validMutationMethodsArr);
- const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
- const validRequestMethods = new Set(validRequestMethodsArr);
- const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
- const redirectPreserveMethodStatusCodes = new Set([307, 308]);
- const IDLE_NAVIGATION = {
- state: "idle",
- location: undefined,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined
- };
- const IDLE_FETCHER = {
- state: "idle",
- data: undefined,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined
- };
- const IDLE_BLOCKER = {
- state: "unblocked",
- proceed: undefined,
- reset: undefined,
- location: undefined
- };
- const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
- const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
- const isServer = !isBrowser; //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region createRouter
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * Create a router and listen to history POP navigations
- */
- function createRouter(init) {
- invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter");
- let dataRoutes = convertRoutesToDataRoutes(init.routes); // Cleanup function for history
- let unlistenHistory = null; // Externally-provided functions to call on all state changes
- let subscribers = new Set(); // Externally-provided object to hold scroll restoration locations during routing
- let savedScrollPositions = null; // Externally-provided function to get scroll restoration keys
- let getScrollRestorationKey = null; // Externally-provided function to get current scroll position
- let getScrollPosition = null; // One-time flag to control the initial hydration scroll restoration. Because
- // we don't get the saved positions from <ScrollRestoration /> until _after_
- // the initial render, we need to manually trigger a separate updateState to
- // send along the restoreScrollPosition
- // Set to true if we have `hydrationData` since we assume we were SSR'd and that
- // SSR did the initial scroll restoration.
- let initialScrollRestored = init.hydrationData != null;
- let initialMatches = matchRoutes(dataRoutes, init.history.location, init.basename);
- let initialErrors = null;
- if (initialMatches == null) {
- // If we do not match a user-provided-route, fall back to the root
- // to allow the error boundary to take over
- let error = getInternalRouterError(404, {
- pathname: init.history.location.pathname
- });
- let {
- matches,
- route
- } = getShortCircuitMatches(dataRoutes);
- initialMatches = matches;
- initialErrors = {
- [route.id]: error
- };
- }
- let initialized = !initialMatches.some(m => m.route.loader) || init.hydrationData != null;
- let router;
- let state = {
- historyAction: init.history.action,
- location: init.history.location,
- matches: initialMatches,
- initialized,
- navigation: IDLE_NAVIGATION,
- // Don't restore on initial updateState() if we were SSR'd
- restoreScrollPosition: init.hydrationData != null ? false : null,
- preventScrollReset: false,
- revalidation: "idle",
- loaderData: init.hydrationData && init.hydrationData.loaderData || {},
- actionData: init.hydrationData && init.hydrationData.actionData || null,
- errors: init.hydrationData && init.hydrationData.errors || initialErrors,
- fetchers: new Map(),
- blockers: new Map()
- }; // -- Stateful internal variables to manage navigations --
- // Current navigation in progress (to be committed in completeNavigation)
- let pendingAction = exports.Action.Pop; // Should the current navigation prevent the scroll reset if scroll cannot
- // be restored?
- let pendingPreventScrollReset = false; // AbortController for the active navigation
- let pendingNavigationController; // We use this to avoid touching history in completeNavigation if a
- // revalidation is entirely uninterrupted
- let isUninterruptedRevalidation = false; // Use this internal flag to force revalidation of all loaders:
- // - submissions (completed or interrupted)
- // - useRevalidate()
- // - X-Remix-Revalidate (from redirect)
- let isRevalidationRequired = false; // Use this internal array to capture routes that require revalidation due
- // to a cancelled deferred on action submission
- let cancelledDeferredRoutes = []; // Use this internal array to capture fetcher loads that were cancelled by an
- // action navigation and require revalidation
- let cancelledFetcherLoads = []; // AbortControllers for any in-flight fetchers
- let fetchControllers = new Map(); // Track loads based on the order in which they started
- let incrementingLoadId = 0; // Track the outstanding pending navigation data load to be compared against
- // the globally incrementing load when a fetcher load lands after a completed
- // navigation
- let pendingNavigationLoadId = -1; // Fetchers that triggered data reloads as a result of their actions
- let fetchReloadIds = new Map(); // Fetchers that triggered redirect navigations from their actions
- let fetchRedirectIds = new Set(); // Most recent href/match for fetcher.load calls for fetchers
- let fetchLoadMatches = new Map(); // Store DeferredData instances for active route matches. When a
- // route loader returns defer() we stick one in here. Then, when a nested
- // promise resolves we update loaderData. If a new navigation starts we
- // cancel active deferreds for eliminated routes.
- let activeDeferreds = new Map(); // Store blocker functions in a separate Map outside of router state since
- // we don't need to update UI state if they change
- let blockerFunctions = new Map(); // Flag to ignore the next history update, so we can revert the URL change on
- // a POP navigation that was blocked by the user without touching router state
- let ignoreNextHistoryUpdate = false; // Initialize the router, all side effects should be kicked off from here.
- // Implemented as a Fluent API for ease of:
- // let router = createRouter(init).initialize();
- function initialize() {
- // If history informs us of a POP navigation, start the navigation but do not update
- // state. We'll update our own state once the navigation completes
- unlistenHistory = init.history.listen(_ref => {
- let {
- action: historyAction,
- location,
- delta
- } = _ref;
- // Ignore this event if it was just us resetting the URL from a
- // blocked POP navigation
- if (ignoreNextHistoryUpdate) {
- ignoreNextHistoryUpdate = false;
- return;
- }
- warning(blockerFunctions.size === 0 || delta != null, "You are trying to use a blocker on a POP navigation to a location " + "that was not created by @remix-run/router. This will fail silently in " + "production. This can happen if you are navigating outside the router " + "via `window.history.pushState`/`window.location.hash` instead of using " + "router navigation APIs. This can also happen if you are using " + "createHashRouter and the user manually changes the URL.");
- let blockerKey = shouldBlockNavigation({
- currentLocation: state.location,
- nextLocation: location,
- historyAction
- });
- if (blockerKey && delta != null) {
- // Restore the URL to match the current UI, but don't update router state
- ignoreNextHistoryUpdate = true;
- init.history.go(delta * -1); // Put the blocker into a blocked state
- updateBlocker(blockerKey, {
- state: "blocked",
- location,
- proceed() {
- updateBlocker(blockerKey, {
- state: "proceeding",
- proceed: undefined,
- reset: undefined,
- location
- }); // Re-do the same POP navigation we just blocked
- init.history.go(delta);
- },
- reset() {
- deleteBlocker(blockerKey);
- updateState({
- blockers: new Map(router.state.blockers)
- });
- }
- });
- return;
- }
- return startNavigation(historyAction, location);
- }); // Kick off initial data load if needed. Use Pop to avoid modifying history
- if (!state.initialized) {
- startNavigation(exports.Action.Pop, state.location);
- }
- return router;
- } // Clean up a router and it's side effects
- function dispose() {
- if (unlistenHistory) {
- unlistenHistory();
- }
- subscribers.clear();
- pendingNavigationController && pendingNavigationController.abort();
- state.fetchers.forEach((_, key) => deleteFetcher(key));
- state.blockers.forEach((_, key) => deleteBlocker(key));
- } // Subscribe to state updates for the router
- function subscribe(fn) {
- subscribers.add(fn);
- return () => subscribers.delete(fn);
- } // Update our state and notify the calling context of the change
- function updateState(newState) {
- state = _extends({}, state, newState);
- subscribers.forEach(subscriber => subscriber(state));
- } // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
- // and setting state.[historyAction/location/matches] to the new route.
- // - Location is a required param
- // - Navigation will always be set to IDLE_NAVIGATION
- // - Can pass any other state in newState
- function completeNavigation(location, newState) {
- var _location$state, _location$state2;
- // Deduce if we're in a loading/actionReload state:
- // - We have committed actionData in the store
- // - The current navigation was a mutation submission
- // - We're past the submitting state and into the loading state
- // - The location being loaded is not the result of a redirect
- let isActionReload = state.actionData != null && state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && state.navigation.state === "loading" && ((_location$state = location.state) == null ? void 0 : _location$state._isRedirect) !== true;
- let actionData;
- if (newState.actionData) {
- if (Object.keys(newState.actionData).length > 0) {
- actionData = newState.actionData;
- } else {
- // Empty actionData -> clear prior actionData due to an action error
- actionData = null;
- }
- } else if (isActionReload) {
- // Keep the current data if we're wrapping up the action reload
- actionData = state.actionData;
- } else {
- // Clear actionData on any other completed navigations
- actionData = null;
- } // Always preserve any existing loaderData from re-used routes
- let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData; // On a successful navigation we can assume we got through all blockers
- // so we can start fresh
- for (let [key] of blockerFunctions) {
- deleteBlocker(key);
- } // Always respect the user flag. Otherwise don't reset on mutation
- // submission navigations unless they redirect
- let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;
- updateState(_extends({}, newState, {
- // matches, errors, fetchers go through as-is
- actionData,
- loaderData,
- historyAction: pendingAction,
- location,
- initialized: true,
- navigation: IDLE_NAVIGATION,
- revalidation: "idle",
- restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
- preventScrollReset,
- blockers: new Map(state.blockers)
- }));
- if (isUninterruptedRevalidation) ; else if (pendingAction === exports.Action.Pop) ; else if (pendingAction === exports.Action.Push) {
- init.history.push(location, location.state);
- } else if (pendingAction === exports.Action.Replace) {
- init.history.replace(location, location.state);
- } // Reset stateful navigation vars
- pendingAction = exports.Action.Pop;
- pendingPreventScrollReset = false;
- isUninterruptedRevalidation = false;
- isRevalidationRequired = false;
- cancelledDeferredRoutes = [];
- cancelledFetcherLoads = [];
- } // Trigger a navigation event, which can either be a numerical POP or a PUSH
- // replace with an optional submission
- async function navigate(to, opts) {
- if (typeof to === "number") {
- init.history.go(to);
- return;
- }
- let {
- path,
- submission,
- error
- } = normalizeNavigateOptions(to, opts);
- let currentLocation = state.location;
- let nextLocation = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
- // URL from window.location, so we need to encode it here so the behavior
- // remains the same as POP and non-data-router usages. new URL() does all
- // the same encoding we'd get from a history.pushState/window.location read
- // without having to touch history
- nextLocation = _extends({}, nextLocation, init.history.encodeLocation(nextLocation));
- let userReplace = opts && opts.replace != null ? opts.replace : undefined;
- let historyAction = exports.Action.Push;
- if (userReplace === true) {
- historyAction = exports.Action.Replace;
- } else if (userReplace === false) ; else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) {
- // By default on submissions to the current location we REPLACE so that
- // users don't have to double-click the back button to get to the prior
- // location. If the user redirects to a different location from the
- // action/loader this will be ignored and the redirect will be a PUSH
- historyAction = exports.Action.Replace;
- }
- let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
- let blockerKey = shouldBlockNavigation({
- currentLocation,
- nextLocation,
- historyAction
- });
- if (blockerKey) {
- // Put the blocker into a blocked state
- updateBlocker(blockerKey, {
- state: "blocked",
- location: nextLocation,
- proceed() {
- updateBlocker(blockerKey, {
- state: "proceeding",
- proceed: undefined,
- reset: undefined,
- location: nextLocation
- }); // Send the same navigation through
- navigate(to, opts);
- },
- reset() {
- deleteBlocker(blockerKey);
- updateState({
- blockers: new Map(state.blockers)
- });
- }
- });
- return;
- }
- return await startNavigation(historyAction, nextLocation, {
- submission,
- // Send through the formData serialization error if we have one so we can
- // render at the right error boundary after we match routes
- pendingError: error,
- preventScrollReset,
- replace: opts && opts.replace
- });
- } // Revalidate all current loaders. If a navigation is in progress or if this
- // is interrupted by a navigation, allow this to "succeed" by calling all
- // loaders during the next loader round
- function revalidate() {
- interruptActiveLoads();
- updateState({
- revalidation: "loading"
- }); // If we're currently submitting an action, we don't need to start a new
- // navigation, we'll just let the follow up loader execution call all loaders
- if (state.navigation.state === "submitting") {
- return;
- } // If we're currently in an idle state, start a new navigation for the current
- // action/location and mark it as uninterrupted, which will skip the history
- // update in completeNavigation
- if (state.navigation.state === "idle") {
- startNavigation(state.historyAction, state.location, {
- startUninterruptedRevalidation: true
- });
- return;
- } // Otherwise, if we're currently in a loading state, just start a new
- // navigation to the navigation.location but do not trigger an uninterrupted
- // revalidation so that history correctly updates once the navigation completes
- startNavigation(pendingAction || state.historyAction, state.navigation.location, {
- overrideNavigation: state.navigation
- });
- } // Start a navigation to the given action/location. Can optionally provide a
- // overrideNavigation which will override the normalLoad in the case of a redirect
- // navigation
- async function startNavigation(historyAction, location, opts) {
- // Abort any in-progress navigations and start a new one. Unset any ongoing
- // uninterrupted revalidations unless told otherwise, since we want this
- // new navigation to update history normally
- pendingNavigationController && pendingNavigationController.abort();
- pendingNavigationController = null;
- pendingAction = historyAction;
- isUninterruptedRevalidation = (opts && opts.startUninterruptedRevalidation) === true; // Save the current scroll position every time we start a new navigation,
- // and track whether we should reset scroll on completion
- saveScrollPosition(state.location, state.matches);
- pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
- let loadingNavigation = opts && opts.overrideNavigation;
- let matches = matchRoutes(dataRoutes, location, init.basename); // Short circuit with a 404 on the root error boundary if we match nothing
- if (!matches) {
- let error = getInternalRouterError(404, {
- pathname: location.pathname
- });
- let {
- matches: notFoundMatches,
- route
- } = getShortCircuitMatches(dataRoutes); // Cancel all pending deferred on 404s since we don't keep any routes
- cancelActiveDeferreds();
- completeNavigation(location, {
- matches: notFoundMatches,
- loaderData: {},
- errors: {
- [route.id]: error
- }
- });
- return;
- } // Short circuit if it's only a hash change and not a mutation submission
- // For example, on /page#hash and submit a <Form method="post"> which will
- // default to a navigation to /page
- if (isHashChangeOnly(state.location, location) && !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))) {
- completeNavigation(location, {
- matches
- });
- return;
- } // Create a controller/Request for this navigation
- pendingNavigationController = new AbortController();
- let request = createClientSideRequest(init.history, location, pendingNavigationController.signal, opts && opts.submission);
- let pendingActionData;
- let pendingError;
- if (opts && opts.pendingError) {
- // If we have a pendingError, it means the user attempted a GET submission
- // with binary FormData so assign here and skip to handleLoaders. That
- // way we handle calling loaders above the boundary etc. It's not really
- // different from an actionError in that sense.
- pendingError = {
- [findNearestBoundary(matches).route.id]: opts.pendingError
- };
- } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
- // Call action if we received an action submission
- let actionOutput = await handleAction(request, location, opts.submission, matches, {
- replace: opts.replace
- });
- if (actionOutput.shortCircuited) {
- return;
- }
- pendingActionData = actionOutput.pendingActionData;
- pendingError = actionOutput.pendingActionError;
- let navigation = _extends({
- state: "loading",
- location
- }, opts.submission);
- loadingNavigation = navigation; // Create a GET request for the loaders
- request = new Request(request.url, {
- signal: request.signal
- });
- } // Call loaders
- let {
- shortCircuited,
- loaderData,
- errors
- } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.replace, pendingActionData, pendingError);
- if (shortCircuited) {
- return;
- } // Clean up now that the action/loaders have completed. Don't clean up if
- // we short circuited because pendingNavigationController will have already
- // been assigned to a new controller for the next navigation
- pendingNavigationController = null;
- completeNavigation(location, _extends({
- matches
- }, pendingActionData ? {
- actionData: pendingActionData
- } : {}, {
- loaderData,
- errors
- }));
- } // Call the action matched by the leaf route for this navigation and handle
- // redirects/errors
- async function handleAction(request, location, submission, matches, opts) {
- interruptActiveLoads(); // Put us in a submitting state
- let navigation = _extends({
- state: "submitting",
- location
- }, submission);
- updateState({
- navigation
- }); // Call our action and get the result
- let result;
- let actionMatch = getTargetMatch(matches, location);
- if (!actionMatch.route.action) {
- result = {
- type: ResultType.error,
- error: getInternalRouterError(405, {
- method: request.method,
- pathname: location.pathname,
- routeId: actionMatch.route.id
- })
- };
- } else {
- result = await callLoaderOrAction("action", request, actionMatch, matches, router.basename);
- if (request.signal.aborted) {
- return {
- shortCircuited: true
- };
- }
- }
- if (isRedirectResult(result)) {
- let replace;
- if (opts && opts.replace != null) {
- replace = opts.replace;
- } else {
- // If the user didn't explicity indicate replace behavior, replace if
- // we redirected to the exact same location we're currently at to avoid
- // double back-buttons
- replace = result.location === state.location.pathname + state.location.search;
- }
- await startRedirectNavigation(state, result, {
- submission,
- replace
- });
- return {
- shortCircuited: true
- };
- }
- if (isErrorResult(result)) {
- // Store off the pending error - we use it to determine which loaders
- // to call and will commit it when we complete the navigation
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id); // By default, all submissions are REPLACE navigations, but if the
- // action threw an error that'll be rendered in an errorElement, we fall
- // back to PUSH so that the user can use the back button to get back to
- // the pre-submission form location to try again
- if ((opts && opts.replace) !== true) {
- pendingAction = exports.Action.Push;
- }
- return {
- // Send back an empty object we can use to clear out any prior actionData
- pendingActionData: {},
- pendingActionError: {
- [boundaryMatch.route.id]: result.error
- }
- };
- }
- if (isDeferredResult(result)) {
- throw getInternalRouterError(400, {
- type: "defer-action"
- });
- }
- return {
- pendingActionData: {
- [actionMatch.route.id]: result.data
- }
- };
- } // Call all applicable loaders for the given matches, handling redirects,
- // errors, etc.
- async function handleLoaders(request, location, matches, overrideNavigation, submission, replace, pendingActionData, pendingError) {
- // Figure out the right navigation we want to use for data loading
- let loadingNavigation = overrideNavigation;
- if (!loadingNavigation) {
- let navigation = _extends({
- state: "loading",
- location,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined
- }, submission);
- loadingNavigation = navigation;
- } // If this was a redirect from an action we don't have a "submission" but
- // we have it on the loading navigation so use that if available
- let activeSubmission = submission ? submission : loadingNavigation.formMethod && loadingNavigation.formAction && loadingNavigation.formData && loadingNavigation.formEncType ? {
- formMethod: loadingNavigation.formMethod,
- formAction: loadingNavigation.formAction,
- formData: loadingNavigation.formData,
- formEncType: loadingNavigation.formEncType
- } : undefined;
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, pendingActionData, pendingError, fetchLoadMatches); // Cancel pending deferreds for no-longer-matched routes or routes we're
- // about to reload. Note that if this is an action reload we would have
- // already cancelled all pending deferreds so this would be a no-op
- cancelActiveDeferreds(routeId => !(matches && matches.some(m => m.route.id === routeId)) || matchesToLoad && matchesToLoad.some(m => m.route.id === routeId)); // Short circuit if we have no loaders to run
- if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
- completeNavigation(location, _extends({
- matches,
- loaderData: {},
- // Commit pending error if we're short circuiting
- errors: pendingError || null
- }, pendingActionData ? {
- actionData: pendingActionData
- } : {}));
- return {
- shortCircuited: true
- };
- } // If this is an uninterrupted revalidation, we remain in our current idle
- // state. If not, we need to switch to our loading state and load data,
- // preserving any new action data or existing action data (in the case of
- // a revalidation interrupting an actionReload)
- if (!isUninterruptedRevalidation) {
- revalidatingFetchers.forEach(rf => {
- let fetcher = state.fetchers.get(rf.key);
- let revalidatingFetcher = {
- state: "loading",
- data: fetcher && fetcher.data,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- " _hasFetcherDoneAnything ": true
- };
- state.fetchers.set(rf.key, revalidatingFetcher);
- });
- let actionData = pendingActionData || state.actionData;
- updateState(_extends({
- navigation: loadingNavigation
- }, actionData ? Object.keys(actionData).length === 0 ? {
- actionData: null
- } : {
- actionData
- } : {}, revalidatingFetchers.length > 0 ? {
- fetchers: new Map(state.fetchers)
- } : {}));
- }
- pendingNavigationLoadId = ++incrementingLoadId;
- revalidatingFetchers.forEach(rf => fetchControllers.set(rf.key, pendingNavigationController));
- let {
- results,
- loaderResults,
- fetcherResults
- } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
- if (request.signal.aborted) {
- return {
- shortCircuited: true
- };
- } // Clean up _after_ loaders have completed. Don't clean up if we short
- // circuited because fetchControllers would have been aborted and
- // reassigned to new controllers for the next navigation
- revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key)); // If any loaders returned a redirect Response, start a new REPLACE navigation
- let redirect = findRedirect(results);
- if (redirect) {
- await startRedirectNavigation(state, redirect, {
- replace
- });
- return {
- shortCircuited: true
- };
- } // Process and commit output from loaders
- let {
- loaderData,
- errors
- } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds); // Wire up subscribers to update loaderData as promises settle
- activeDeferreds.forEach((deferredData, routeId) => {
- deferredData.subscribe(aborted => {
- // Note: No need to updateState here since the TrackedPromise on
- // loaderData is stable across resolve/reject
- // Remove this instance if we were aborted or if promises have settled
- if (aborted || deferredData.done) {
- activeDeferreds.delete(routeId);
- }
- });
- });
- markFetchRedirectsDone();
- let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
- return _extends({
- loaderData,
- errors
- }, didAbortFetchLoads || revalidatingFetchers.length > 0 ? {
- fetchers: new Map(state.fetchers)
- } : {});
- }
- function getFetcher(key) {
- return state.fetchers.get(key) || IDLE_FETCHER;
- } // Trigger a fetcher load/submit for the given fetcher key
- function fetch(key, routeId, href, opts) {
- if (isServer) {
- throw new Error("router.fetch() was called during the server render, but it shouldn't be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");
- }
- if (fetchControllers.has(key)) abortFetcher(key);
- let matches = matchRoutes(dataRoutes, href, init.basename);
- if (!matches) {
- setFetcherError(key, routeId, getInternalRouterError(404, {
- pathname: href
- }));
- return;
- }
- let {
- path,
- submission
- } = normalizeNavigateOptions(href, opts, true);
- let match = getTargetMatch(matches, path);
- pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
- if (submission && isMutationMethod(submission.formMethod)) {
- handleFetcherAction(key, routeId, path, match, matches, submission);
- return;
- } // Store off the match so we can call it's shouldRevalidate on subsequent
- // revalidations
- fetchLoadMatches.set(key, {
- routeId,
- path,
- match,
- matches
- });
- handleFetcherLoader(key, routeId, path, match, matches, submission);
- } // Call the action for the matched fetcher.submit(), and then handle redirects,
- // errors, and revalidation
- async function handleFetcherAction(key, routeId, path, match, requestMatches, submission) {
- interruptActiveLoads();
- fetchLoadMatches.delete(key);
- if (!match.route.action) {
- let error = getInternalRouterError(405, {
- method: submission.formMethod,
- pathname: path,
- routeId: routeId
- });
- setFetcherError(key, routeId, error);
- return;
- } // Put this fetcher into it's submitting state
- let existingFetcher = state.fetchers.get(key);
- let fetcher = _extends({
- state: "submitting"
- }, submission, {
- data: existingFetcher && existingFetcher.data,
- " _hasFetcherDoneAnything ": true
- });
- state.fetchers.set(key, fetcher);
- updateState({
- fetchers: new Map(state.fetchers)
- }); // Call the action for the fetcher
- let abortController = new AbortController();
- let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
- fetchControllers.set(key, abortController);
- let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, router.basename);
- if (fetchRequest.signal.aborted) {
- // We can delete this so long as we weren't aborted by ou our own fetcher
- // re-submit which would have put _new_ controller is in fetchControllers
- if (fetchControllers.get(key) === abortController) {
- fetchControllers.delete(key);
- }
- return;
- }
- if (isRedirectResult(actionResult)) {
- fetchControllers.delete(key);
- fetchRedirectIds.add(key);
- let loadingFetcher = _extends({
- state: "loading"
- }, submission, {
- data: undefined,
- " _hasFetcherDoneAnything ": true
- });
- state.fetchers.set(key, loadingFetcher);
- updateState({
- fetchers: new Map(state.fetchers)
- });
- return startRedirectNavigation(state, actionResult, {
- isFetchActionRedirect: true
- });
- } // Process any non-redirect errors thrown
- if (isErrorResult(actionResult)) {
- setFetcherError(key, routeId, actionResult.error);
- return;
- }
- if (isDeferredResult(actionResult)) {
- throw getInternalRouterError(400, {
- type: "defer-action"
- });
- } // Start the data load for current matches, or the next location if we're
- // in the middle of a navigation
- let nextLocation = state.navigation.location || state.location;
- let revalidationRequest = createClientSideRequest(init.history, nextLocation, abortController.signal);
- let matches = state.navigation.state !== "idle" ? matchRoutes(dataRoutes, state.navigation.location, init.basename) : state.matches;
- invariant(matches, "Didn't find any matches after fetcher action");
- let loadId = ++incrementingLoadId;
- fetchReloadIds.set(key, loadId);
- let loadFetcher = _extends({
- state: "loading",
- data: actionResult.data
- }, submission, {
- " _hasFetcherDoneAnything ": true
- });
- state.fetchers.set(key, loadFetcher);
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, {
- [match.route.id]: actionResult.data
- }, undefined, // No need to send through errors since we short circuit above
- fetchLoadMatches); // Put all revalidating fetchers into the loading state, except for the
- // current fetcher which we want to keep in it's current loading state which
- // contains it's action submission info + action data
- revalidatingFetchers.filter(rf => rf.key !== key).forEach(rf => {
- let staleKey = rf.key;
- let existingFetcher = state.fetchers.get(staleKey);
- let revalidatingFetcher = {
- state: "loading",
- data: existingFetcher && existingFetcher.data,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- " _hasFetcherDoneAnything ": true
- };
- state.fetchers.set(staleKey, revalidatingFetcher);
- fetchControllers.set(staleKey, abortController);
- });
- updateState({
- fetchers: new Map(state.fetchers)
- });
- let {
- results,
- loaderResults,
- fetcherResults
- } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
- if (abortController.signal.aborted) {
- return;
- }
- fetchReloadIds.delete(key);
- fetchControllers.delete(key);
- revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
- let redirect = findRedirect(results);
- if (redirect) {
- return startRedirectNavigation(state, redirect);
- } // Process and commit output from loaders
- let {
- loaderData,
- errors
- } = processLoaderData(state, state.matches, matchesToLoad, loaderResults, undefined, revalidatingFetchers, fetcherResults, activeDeferreds);
- let doneFetcher = {
- state: "idle",
- data: actionResult.data,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- " _hasFetcherDoneAnything ": true
- };
- state.fetchers.set(key, doneFetcher);
- let didAbortFetchLoads = abortStaleFetchLoads(loadId); // If we are currently in a navigation loading state and this fetcher is
- // more recent than the navigation, we want the newer data so abort the
- // navigation and complete it with the fetcher data
- if (state.navigation.state === "loading" && loadId > pendingNavigationLoadId) {
- invariant(pendingAction, "Expected pending action");
- pendingNavigationController && pendingNavigationController.abort();
- completeNavigation(state.navigation.location, {
- matches,
- loaderData,
- errors,
- fetchers: new Map(state.fetchers)
- });
- } else {
- // otherwise just update with the fetcher data, preserving any existing
- // loaderData for loaders that did not need to reload. We have to
- // manually merge here since we aren't going through completeNavigation
- updateState(_extends({
- errors,
- loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors)
- }, didAbortFetchLoads ? {
- fetchers: new Map(state.fetchers)
- } : {}));
- isRevalidationRequired = false;
- }
- } // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
- async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
- let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state
- let loadingFetcher = _extends({
- state: "loading",
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined
- }, submission, {
- data: existingFetcher && existingFetcher.data,
- " _hasFetcherDoneAnything ": true
- });
- state.fetchers.set(key, loadingFetcher);
- updateState({
- fetchers: new Map(state.fetchers)
- }); // Call the loader for this fetcher route match
- let abortController = new AbortController();
- let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
- fetchControllers.set(key, abortController);
- let result = await callLoaderOrAction("loader", fetchRequest, match, matches, router.basename); // Deferred isn't supported for fetcher loads, await everything and treat it
- // as a normal load. resolveDeferredData will return undefined if this
- // fetcher gets aborted, so we just leave result untouched and short circuit
- // below if that happens
- if (isDeferredResult(result)) {
- result = (await resolveDeferredData(result, fetchRequest.signal, true)) || result;
- } // We can delete this so long as we weren't aborted by ou our own fetcher
- // re-load which would have put _new_ controller is in fetchControllers
- if (fetchControllers.get(key) === abortController) {
- fetchControllers.delete(key);
- }
- if (fetchRequest.signal.aborted) {
- return;
- } // If the loader threw a redirect Response, start a new REPLACE navigation
- if (isRedirectResult(result)) {
- await startRedirectNavigation(state, result);
- return;
- } // Process any non-redirect errors thrown
- if (isErrorResult(result)) {
- let boundaryMatch = findNearestBoundary(state.matches, routeId);
- state.fetchers.delete(key); // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
- // do we need to behave any differently with our non-redirect errors?
- // What if it was a non-redirect Response?
- updateState({
- fetchers: new Map(state.fetchers),
- errors: {
- [boundaryMatch.route.id]: result.error
- }
- });
- return;
- }
- invariant(!isDeferredResult(result), "Unhandled fetcher deferred data"); // Put the fetcher back into an idle state
- let doneFetcher = {
- state: "idle",
- data: result.data,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- " _hasFetcherDoneAnything ": true
- };
- state.fetchers.set(key, doneFetcher);
- updateState({
- fetchers: new Map(state.fetchers)
- });
- }
- /**
- * Utility function to handle redirects returned from an action or loader.
- * Normally, a redirect "replaces" the navigation that triggered it. So, for
- * example:
- *
- * - user is on /a
- * - user clicks a link to /b
- * - loader for /b redirects to /c
- *
- * In a non-JS app the browser would track the in-flight navigation to /b and
- * then replace it with /c when it encountered the redirect response. In
- * the end it would only ever update the URL bar with /c.
- *
- * In client-side routing using pushState/replaceState, we aim to emulate
- * this behavior and we also do not update history until the end of the
- * navigation (including processed redirects). This means that we never
- * actually touch history until we've processed redirects, so we just use
- * the history action from the original navigation (PUSH or REPLACE).
- */
- async function startRedirectNavigation(state, redirect, _temp) {
- var _window;
- let {
- submission,
- replace,
- isFetchActionRedirect
- } = _temp === void 0 ? {} : _temp;
- if (redirect.revalidate) {
- isRevalidationRequired = true;
- }
- let redirectLocation = createLocation(state.location, redirect.location, // TODO: This can be removed once we get rid of useTransition in Remix v2
- _extends({
- _isRedirect: true
- }, isFetchActionRedirect ? {
- _isFetchActionRedirect: true
- } : {}));
- invariant(redirectLocation, "Expected a location on the redirect navigation"); // Check if this an absolute external redirect that goes to a new origin
- if (ABSOLUTE_URL_REGEX.test(redirect.location) && isBrowser && typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {
- let newOrigin = init.history.createURL(redirect.location).origin;
- if (window.location.origin !== newOrigin) {
- if (replace) {
- window.location.replace(redirect.location);
- } else {
- window.location.assign(redirect.location);
- }
- return;
- }
- } // There's no need to abort on redirects, since we don't detect the
- // redirect until the action/loaders have settled
- pendingNavigationController = null;
- let redirectHistoryAction = replace === true ? exports.Action.Replace : exports.Action.Push; // Use the incoming submission if provided, fallback on the active one in
- // state.navigation
- let {
- formMethod,
- formAction,
- formEncType,
- formData
- } = state.navigation;
- if (!submission && formMethod && formAction && formData && formEncType) {
- submission = {
- formMethod,
- formAction,
- formEncType,
- formData
- };
- } // If this was a 307/308 submission we want to preserve the HTTP method and
- // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
- // redirected location
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && submission && isMutationMethod(submission.formMethod)) {
- await startNavigation(redirectHistoryAction, redirectLocation, {
- submission: _extends({}, submission, {
- formAction: redirect.location
- }),
- // Preserve this flag across redirects
- preventScrollReset: pendingPreventScrollReset
- });
- } else {
- // Otherwise, we kick off a new loading navigation, preserving the
- // submission info for the duration of this navigation
- await startNavigation(redirectHistoryAction, redirectLocation, {
- overrideNavigation: {
- state: "loading",
- location: redirectLocation,
- formMethod: submission ? submission.formMethod : undefined,
- formAction: submission ? submission.formAction : undefined,
- formEncType: submission ? submission.formEncType : undefined,
- formData: submission ? submission.formData : undefined
- },
- // Preserve this flag across redirects
- preventScrollReset: pendingPreventScrollReset
- });
- }
- }
- async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
- // Call all navigation loaders and revalidating fetcher loaders in parallel,
- // then slice off the results into separate arrays so we can handle them
- // accordingly
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, router.basename)), ...fetchersToLoad.map(f => callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, request.signal), f.match, f.matches, router.basename))]);
- let loaderResults = results.slice(0, matchesToLoad.length);
- let fetcherResults = results.slice(matchesToLoad.length);
- await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, request.signal, false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(f => f.match), fetcherResults, request.signal, true)]);
- return {
- results,
- loaderResults,
- fetcherResults
- };
- }
- function interruptActiveLoads() {
- // Every interruption triggers a revalidation
- isRevalidationRequired = true; // Cancel pending route-level deferreds and mark cancelled routes for
- // revalidation
- cancelledDeferredRoutes.push(...cancelActiveDeferreds()); // Abort in-flight fetcher loads
- fetchLoadMatches.forEach((_, key) => {
- if (fetchControllers.has(key)) {
- cancelledFetcherLoads.push(key);
- abortFetcher(key);
- }
- });
- }
- function setFetcherError(key, routeId, error) {
- let boundaryMatch = findNearestBoundary(state.matches, routeId);
- deleteFetcher(key);
- updateState({
- errors: {
- [boundaryMatch.route.id]: error
- },
- fetchers: new Map(state.fetchers)
- });
- }
- function deleteFetcher(key) {
- if (fetchControllers.has(key)) abortFetcher(key);
- fetchLoadMatches.delete(key);
- fetchReloadIds.delete(key);
- fetchRedirectIds.delete(key);
- state.fetchers.delete(key);
- }
- function abortFetcher(key) {
- let controller = fetchControllers.get(key);
- invariant(controller, "Expected fetch controller: " + key);
- controller.abort();
- fetchControllers.delete(key);
- }
- function markFetchersDone(keys) {
- for (let key of keys) {
- let fetcher = getFetcher(key);
- let doneFetcher = {
- state: "idle",
- data: fetcher.data,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- " _hasFetcherDoneAnything ": true
- };
- state.fetchers.set(key, doneFetcher);
- }
- }
- function markFetchRedirectsDone() {
- let doneKeys = [];
- for (let key of fetchRedirectIds) {
- let fetcher = state.fetchers.get(key);
- invariant(fetcher, "Expected fetcher: " + key);
- if (fetcher.state === "loading") {
- fetchRedirectIds.delete(key);
- doneKeys.push(key);
- }
- }
- markFetchersDone(doneKeys);
- }
- function abortStaleFetchLoads(landedId) {
- let yeetedKeys = [];
- for (let [key, id] of fetchReloadIds) {
- if (id < landedId) {
- let fetcher = state.fetchers.get(key);
- invariant(fetcher, "Expected fetcher: " + key);
- if (fetcher.state === "loading") {
- abortFetcher(key);
- fetchReloadIds.delete(key);
- yeetedKeys.push(key);
- }
- }
- }
- markFetchersDone(yeetedKeys);
- return yeetedKeys.length > 0;
- }
- function getBlocker(key, fn) {
- let blocker = state.blockers.get(key) || IDLE_BLOCKER;
- if (blockerFunctions.get(key) !== fn) {
- blockerFunctions.set(key, fn);
- }
- return blocker;
- }
- function deleteBlocker(key) {
- state.blockers.delete(key);
- blockerFunctions.delete(key);
- } // Utility function to update blockers, ensuring valid state transitions
- function updateBlocker(key, newBlocker) {
- let blocker = state.blockers.get(key) || IDLE_BLOCKER; // Poor mans state machine :)
- // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
- invariant(blocker.state === "unblocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "proceeding" || blocker.state === "blocked" && newBlocker.state === "unblocked" || blocker.state === "proceeding" && newBlocker.state === "unblocked", "Invalid blocker state transition: " + blocker.state + " -> " + newBlocker.state);
- state.blockers.set(key, newBlocker);
- updateState({
- blockers: new Map(state.blockers)
- });
- }
- function shouldBlockNavigation(_ref2) {
- let {
- currentLocation,
- nextLocation,
- historyAction
- } = _ref2;
- if (blockerFunctions.size === 0) {
- return;
- } // We ony support a single active blocker at the moment since we don't have
- // any compelling use cases for multi-blocker yet
- if (blockerFunctions.size > 1) {
- warning(false, "A router only supports one blocker at a time");
- }
- let entries = Array.from(blockerFunctions.entries());
- let [blockerKey, blockerFunction] = entries[entries.length - 1];
- let blocker = state.blockers.get(blockerKey);
- if (blocker && blocker.state === "proceeding") {
- // If the blocker is currently proceeding, we don't need to re-check
- // it and can let this navigation continue
- return;
- } // At this point, we know we're unblocked/blocked so we need to check the
- // user-provided blocker function
- if (blockerFunction({
- currentLocation,
- nextLocation,
- historyAction
- })) {
- return blockerKey;
- }
- }
- function cancelActiveDeferreds(predicate) {
- let cancelledRouteIds = [];
- activeDeferreds.forEach((dfd, routeId) => {
- if (!predicate || predicate(routeId)) {
- // Cancel the deferred - but do not remove from activeDeferreds here -
- // we rely on the subscribers to do that so our tests can assert proper
- // cleanup via _internalActiveDeferreds
- dfd.cancel();
- cancelledRouteIds.push(routeId);
- activeDeferreds.delete(routeId);
- }
- });
- return cancelledRouteIds;
- } // Opt in to capturing and reporting scroll positions during navigations,
- // used by the <ScrollRestoration> component
- function enableScrollRestoration(positions, getPosition, getKey) {
- savedScrollPositions = positions;
- getScrollPosition = getPosition;
- getScrollRestorationKey = getKey || (location => location.key); // Perform initial hydration scroll restoration, since we miss the boat on
- // the initial updateState() because we've not yet rendered <ScrollRestoration/>
- // and therefore have no savedScrollPositions available
- if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
- initialScrollRestored = true;
- let y = getSavedScrollPosition(state.location, state.matches);
- if (y != null) {
- updateState({
- restoreScrollPosition: y
- });
- }
- }
- return () => {
- savedScrollPositions = null;
- getScrollPosition = null;
- getScrollRestorationKey = null;
- };
- }
- function saveScrollPosition(location, matches) {
- if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
- let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData));
- let key = getScrollRestorationKey(location, userMatches) || location.key;
- savedScrollPositions[key] = getScrollPosition();
- }
- }
- function getSavedScrollPosition(location, matches) {
- if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
- let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData));
- let key = getScrollRestorationKey(location, userMatches) || location.key;
- let y = savedScrollPositions[key];
- if (typeof y === "number") {
- return y;
- }
- }
- return null;
- }
- router = {
- get basename() {
- return init.basename;
- },
- get state() {
- return state;
- },
- get routes() {
- return dataRoutes;
- },
- initialize,
- subscribe,
- enableScrollRestoration,
- navigate,
- fetch,
- revalidate,
- // Passthrough to history-aware createHref used by useHref so we get proper
- // hash-aware URLs in DOM paths
- createHref: to => init.history.createHref(to),
- encodeLocation: to => init.history.encodeLocation(to),
- getFetcher,
- deleteFetcher,
- dispose,
- getBlocker,
- deleteBlocker,
- _internalFetchControllers: fetchControllers,
- _internalActiveDeferreds: activeDeferreds
- };
- return router;
- } //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region createStaticHandler
- ////////////////////////////////////////////////////////////////////////////////
- const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
- function createStaticHandler(routes, opts) {
- invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
- let dataRoutes = convertRoutesToDataRoutes(routes);
- let basename = (opts ? opts.basename : null) || "/";
- /**
- * The query() method is intended for document requests, in which we want to
- * call an optional action and potentially multiple loaders for all nested
- * routes. It returns a StaticHandlerContext object, which is very similar
- * to the router state (location, loaderData, actionData, errors, etc.) and
- * also adds SSR-specific information such as the statusCode and headers
- * from action/loaders Responses.
- *
- * It _should_ never throw and should report all errors through the
- * returned context.errors object, properly associating errors to their error
- * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
- * used to emulate React error boundaries during SSr by performing a second
- * pass only down to the boundaryId.
- *
- * The one exception where we do not return a StaticHandlerContext is when a
- * redirect response is returned or thrown from any action/loader. We
- * propagate that out and return the raw Response so the HTTP server can
- * return it directly.
- */
- async function query(request, _temp2) {
- let {
- requestContext
- } = _temp2 === void 0 ? {} : _temp2;
- let url = new URL(request.url);
- let method = request.method.toLowerCase();
- let location = createLocation("", createPath(url), null, "default");
- let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
- if (!isValidMethod(method) && method !== "head") {
- let error = getInternalRouterError(405, {
- method
- });
- let {
- matches: methodNotAllowedMatches,
- route
- } = getShortCircuitMatches(dataRoutes);
- return {
- basename,
- location,
- matches: methodNotAllowedMatches,
- loaderData: {},
- actionData: null,
- errors: {
- [route.id]: error
- },
- statusCode: error.status,
- loaderHeaders: {},
- actionHeaders: {},
- activeDeferreds: null
- };
- } else if (!matches) {
- let error = getInternalRouterError(404, {
- pathname: location.pathname
- });
- let {
- matches: notFoundMatches,
- route
- } = getShortCircuitMatches(dataRoutes);
- return {
- basename,
- location,
- matches: notFoundMatches,
- loaderData: {},
- actionData: null,
- errors: {
- [route.id]: error
- },
- statusCode: error.status,
- loaderHeaders: {},
- actionHeaders: {},
- activeDeferreds: null
- };
- }
- let result = await queryImpl(request, location, matches, requestContext);
- if (isResponse(result)) {
- return result;
- } // When returning StaticHandlerContext, we patch back in the location here
- // since we need it for React Context. But this helps keep our submit and
- // loadRouteData operating on a Request instead of a Location
- return _extends({
- location,
- basename
- }, result);
- }
- /**
- * The queryRoute() method is intended for targeted route requests, either
- * for fetch ?_data requests or resource route requests. In this case, we
- * are only ever calling a single action or loader, and we are returning the
- * returned value directly. In most cases, this will be a Response returned
- * from the action/loader, but it may be a primitive or other value as well -
- * and in such cases the calling context should handle that accordingly.
- *
- * We do respect the throw/return differentiation, so if an action/loader
- * throws, then this method will throw the value. This is important so we
- * can do proper boundary identification in Remix where a thrown Response
- * must go to the Catch Boundary but a returned Response is happy-path.
- *
- * One thing to note is that any Router-initiated Errors that make sense
- * to associate with a status code will be thrown as an ErrorResponse
- * instance which include the raw Error, such that the calling context can
- * serialize the error as they see fit while including the proper response
- * code. Examples here are 404 and 405 errors that occur prior to reaching
- * any user-defined loaders.
- */
- async function queryRoute(request, _temp3) {
- let {
- routeId,
- requestContext
- } = _temp3 === void 0 ? {} : _temp3;
- let url = new URL(request.url);
- let method = request.method.toLowerCase();
- let location = createLocation("", createPath(url), null, "default");
- let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't
- if (!isValidMethod(method) && method !== "head" && method !== "options") {
- throw getInternalRouterError(405, {
- method
- });
- } else if (!matches) {
- throw getInternalRouterError(404, {
- pathname: location.pathname
- });
- }
- let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);
- if (routeId && !match) {
- throw getInternalRouterError(403, {
- pathname: location.pathname,
- routeId
- });
- } else if (!match) {
- // This should never hit I don't think?
- throw getInternalRouterError(404, {
- pathname: location.pathname
- });
- }
- let result = await queryImpl(request, location, matches, requestContext, match);
- if (isResponse(result)) {
- return result;
- }
- let error = result.errors ? Object.values(result.errors)[0] : undefined;
- if (error !== undefined) {
- // If we got back result.errors, that means the loader/action threw
- // _something_ that wasn't a Response, but it's not guaranteed/required
- // to be an `instanceof Error` either, so we have to use throw here to
- // preserve the "error" state outside of queryImpl.
- throw error;
- } // Pick off the right state value to return
- if (result.actionData) {
- return Object.values(result.actionData)[0];
- }
- if (result.loaderData) {
- var _result$activeDeferre;
- let data = Object.values(result.loaderData)[0];
- if ((_result$activeDeferre = result.activeDeferreds) != null && _result$activeDeferre[match.route.id]) {
- data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
- }
- return data;
- }
- return undefined;
- }
- async function queryImpl(request, location, matches, requestContext, routeMatch) {
- invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
- try {
- if (isMutationMethod(request.method.toLowerCase())) {
- let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
- return result;
- }
- let result = await loadRouteData(request, matches, requestContext, routeMatch);
- return isResponse(result) ? result : _extends({}, result, {
- actionData: null,
- actionHeaders: {}
- });
- } catch (e) {
- // If the user threw/returned a Response in callLoaderOrAction, we throw
- // it to bail out and then return or throw here based on whether the user
- // returned or threw
- if (isQueryRouteResponse(e)) {
- if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
- throw e.response;
- }
- return e.response;
- } // Redirects are always returned since they don't propagate to catch
- // boundaries
- if (isRedirectResponse(e)) {
- return e;
- }
- throw e;
- }
- }
- async function submit(request, matches, actionMatch, requestContext, isRouteRequest) {
- let result;
- if (!actionMatch.route.action) {
- let error = getInternalRouterError(405, {
- method: request.method,
- pathname: new URL(request.url).pathname,
- routeId: actionMatch.route.id
- });
- if (isRouteRequest) {
- throw error;
- }
- result = {
- type: ResultType.error,
- error
- };
- } else {
- result = await callLoaderOrAction("action", request, actionMatch, matches, basename, true, isRouteRequest, requestContext);
- if (request.signal.aborted) {
- let method = isRouteRequest ? "queryRoute" : "query";
- throw new Error(method + "() call aborted");
- }
- }
- if (isRedirectResult(result)) {
- // Uhhhh - this should never happen, we should always throw these from
- // callLoaderOrAction, but the type narrowing here keeps TS happy and we
- // can get back on the "throw all redirect responses" train here should
- // this ever happen :/
- throw new Response(null, {
- status: result.status,
- headers: {
- Location: result.location
- }
- });
- }
- if (isDeferredResult(result)) {
- let error = getInternalRouterError(400, {
- type: "defer-action"
- });
- if (isRouteRequest) {
- throw error;
- }
- result = {
- type: ResultType.error,
- error
- };
- }
- if (isRouteRequest) {
- // Note: This should only be non-Response values if we get here, since
- // isRouteRequest should throw any Response received in callLoaderOrAction
- if (isErrorResult(result)) {
- throw result.error;
- }
- return {
- matches: [actionMatch],
- loaderData: {},
- actionData: {
- [actionMatch.route.id]: result.data
- },
- errors: null,
- // Note: statusCode + headers are unused here since queryRoute will
- // return the raw Response or value
- statusCode: 200,
- loaderHeaders: {},
- actionHeaders: {},
- activeDeferreds: null
- };
- }
- if (isErrorResult(result)) {
- // Store off the pending error - we use it to determine which loaders
- // to call and will commit it when we complete the navigation
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
- let context = await loadRouteData(request, matches, requestContext, undefined, {
- [boundaryMatch.route.id]: result.error
- }); // action status codes take precedence over loader status codes
- return _extends({}, context, {
- statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,
- actionData: null,
- actionHeaders: _extends({}, result.headers ? {
- [actionMatch.route.id]: result.headers
- } : {})
- });
- } // Create a GET request for the loaders
- let loaderRequest = new Request(request.url, {
- headers: request.headers,
- redirect: request.redirect,
- signal: request.signal
- });
- let context = await loadRouteData(loaderRequest, matches, requestContext);
- return _extends({}, context, result.statusCode ? {
- statusCode: result.statusCode
- } : {}, {
- actionData: {
- [actionMatch.route.id]: result.data
- },
- actionHeaders: _extends({}, result.headers ? {
- [actionMatch.route.id]: result.headers
- } : {})
- });
- }
- async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {
- let isRouteRequest = routeMatch != null; // Short circuit if we have no loaders to run (queryRoute())
- if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader)) {
- throw getInternalRouterError(400, {
- method: request.method,
- pathname: new URL(request.url).pathname,
- routeId: routeMatch == null ? void 0 : routeMatch.route.id
- });
- }
- let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
- let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run (query())
- if (matchesToLoad.length === 0) {
- return {
- matches,
- // Add a null for all matched routes for proper revalidation on the client
- loaderData: matches.reduce((acc, m) => Object.assign(acc, {
- [m.route.id]: null
- }), {}),
- errors: pendingActionError || null,
- statusCode: 200,
- loaderHeaders: {},
- activeDeferreds: null
- };
- }
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, basename, true, isRouteRequest, requestContext))]);
- if (request.signal.aborted) {
- let method = isRouteRequest ? "queryRoute" : "query";
- throw new Error(method + "() call aborted");
- } // Process and commit output from loaders
- let activeDeferreds = new Map();
- let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError, activeDeferreds); // Add a null for any non-loader matches for proper revalidation on the client
- let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));
- matches.forEach(match => {
- if (!executedLoaders.has(match.route.id)) {
- context.loaderData[match.route.id] = null;
- }
- });
- return _extends({}, context, {
- matches,
- activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
- });
- }
- return {
- dataRoutes,
- query,
- queryRoute
- };
- } //#endregion
- ////////////////////////////////////////////////////////////////////////////////
- //#region Helpers
- ////////////////////////////////////////////////////////////////////////////////
- /**
- * Given an existing StaticHandlerContext and an error thrown at render time,
- * provide an updated StaticHandlerContext suitable for a second SSR render
- */
- function getStaticContextFromError(routes, context, error) {
- let newContext = _extends({}, context, {
- statusCode: 500,
- errors: {
- [context._deepestRenderedBoundaryId || routes[0].id]: error
- }
- });
- return newContext;
- }
- function isSubmissionNavigation(opts) {
- return opts != null && "formData" in opts;
- } // Normalize navigation options by converting formMethod=GET formData objects to
- // URLSearchParams so they behave identically to links with query params
- function normalizeNavigateOptions(to, opts, isFetcher) {
- if (isFetcher === void 0) {
- isFetcher = false;
- }
- let path = typeof to === "string" ? to : createPath(to); // Return location verbatim on non-submission navigations
- if (!opts || !isSubmissionNavigation(opts)) {
- return {
- path
- };
- }
- if (opts.formMethod && !isValidMethod(opts.formMethod)) {
- return {
- path,
- error: getInternalRouterError(405, {
- method: opts.formMethod
- })
- };
- } // Create a Submission on non-GET navigations
- let submission;
- if (opts.formData) {
- submission = {
- formMethod: opts.formMethod || "get",
- formAction: stripHashFromPath(path),
- formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
- formData: opts.formData
- };
- if (isMutationMethod(submission.formMethod)) {
- return {
- path,
- submission
- };
- }
- } // Flatten submission onto URLSearchParams for GET submissions
- let parsedPath = parsePath(path);
- let searchParams = convertFormDataToSearchParams(opts.formData); // Since fetcher GET submissions only run a single loader (as opposed to
- // navigation GET submissions which run all loaders), we need to preserve
- // any incoming ?index params
- if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
- searchParams.append("index", "");
- }
- parsedPath.search = "?" + searchParams;
- return {
- path: createPath(parsedPath),
- submission
- };
- } // Filter out all routes below any caught error as they aren't going to
- // render so we don't need to load them
- function getLoaderMatchesUntilBoundary(matches, boundaryId) {
- let boundaryMatches = matches;
- if (boundaryId) {
- let index = matches.findIndex(m => m.route.id === boundaryId);
- if (index >= 0) {
- boundaryMatches = matches.slice(0, index);
- }
- }
- return boundaryMatches;
- }
- function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, pendingActionData, pendingError, fetchLoadMatches) {
- let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
- let currentUrl = history.createURL(state.location);
- let nextUrl = history.createURL(location);
- let defaultShouldRevalidate = // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
- isRevalidationRequired || // Clicked the same link, resubmitted a GET form
- currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
- currentUrl.search !== nextUrl.search; // Pick navigation matches that are net-new or qualify for revalidation
- let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
- let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
- let navigationMatches = boundaryMatches.filter((match, index) => {
- if (match.route.loader == null) {
- return false;
- } // Always call the loader on new route instances and pending defer cancellations
- if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
- return true;
- } // This is the default implementation for when we revalidate. If the route
- // provides it's own implementation, then we give them full control but
- // provide this value so they can leverage it if needed after they check
- // their own specific use cases
- let currentRouteMatch = state.matches[index];
- let nextRouteMatch = match;
- return shouldRevalidateLoader(match, _extends({
- currentUrl,
- currentParams: currentRouteMatch.params,
- nextUrl,
- nextParams: nextRouteMatch.params
- }, submission, {
- actionResult,
- defaultShouldRevalidate: defaultShouldRevalidate || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
- }));
- }); // Pick fetcher.loads that need to be revalidated
- let revalidatingFetchers = [];
- fetchLoadMatches && fetchLoadMatches.forEach((f, key) => {
- if (!matches.some(m => m.route.id === f.routeId)) {
- // This fetcher is not going to be present in the subsequent render so
- // there's no need to revalidate it
- return;
- } else if (cancelledFetcherLoads.includes(key)) {
- // This fetcher was cancelled from a prior action submission - force reload
- revalidatingFetchers.push(_extends({
- key
- }, f));
- } else {
- // Revalidating fetchers are decoupled from the route matches since they
- // hit a static href, so they _always_ check shouldRevalidate and the
- // default is strictly if a revalidation is explicitly required (action
- // submissions, useRevalidator, X-Remix-Revalidate).
- let shouldRevalidate = shouldRevalidateLoader(f.match, _extends({
- currentUrl,
- currentParams: state.matches[state.matches.length - 1].params,
- nextUrl,
- nextParams: matches[matches.length - 1].params
- }, submission, {
- actionResult,
- defaultShouldRevalidate
- }));
- if (shouldRevalidate) {
- revalidatingFetchers.push(_extends({
- key
- }, f));
- }
- }
- });
- return [navigationMatches, revalidatingFetchers];
- }
- function isNewLoader(currentLoaderData, currentMatch, match) {
- let isNew = // [a] -> [a, b]
- !currentMatch || // [a, b] -> [a, c]
- match.route.id !== currentMatch.route.id; // Handle the case that we don't have data for a re-used route, potentially
- // from a prior error or from a cancelled pending deferred
- let isMissingData = currentLoaderData[match.route.id] === undefined; // Always load if this is a net-new route or we don't yet have data
- return isNew || isMissingData;
- }
- function isNewRouteInstance(currentMatch, match) {
- let currentPath = currentMatch.route.path;
- return (// param change for this match, /users/123 -> /users/456
- currentMatch.pathname !== match.pathname || // splat param changed, which is not present in match.path
- // e.g. /files/images/avatar.jpg -> files/finances.xls
- currentPath != null && currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"]
- );
- }
- function shouldRevalidateLoader(loaderMatch, arg) {
- if (loaderMatch.route.shouldRevalidate) {
- let routeChoice = loaderMatch.route.shouldRevalidate(arg);
- if (typeof routeChoice === "boolean") {
- return routeChoice;
- }
- }
- return arg.defaultShouldRevalidate;
- }
- async function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest, requestContext) {
- if (basename === void 0) {
- basename = "/";
- }
- if (isStaticRequest === void 0) {
- isStaticRequest = false;
- }
- if (isRouteRequest === void 0) {
- isRouteRequest = false;
- }
- let resultType;
- let result; // Setup a promise we can race against so that abort signals short circuit
- let reject;
- let abortPromise = new Promise((_, r) => reject = r);
- let onReject = () => reject();
- request.signal.addEventListener("abort", onReject);
- try {
- let handler = match.route[type];
- invariant(handler, "Could not find the " + type + " to run on the \"" + match.route.id + "\" route");
- result = await Promise.race([handler({
- request,
- params: match.params,
- context: requestContext
- }), abortPromise]);
- invariant(result !== undefined, "You defined " + (type === "action" ? "an action" : "a loader") + " for route " + ("\"" + match.route.id + "\" but didn't return anything from your `" + type + "` ") + "function. Please return a value or `null`.");
- } catch (e) {
- resultType = ResultType.error;
- result = e;
- } finally {
- request.signal.removeEventListener("abort", onReject);
- }
- if (isResponse(result)) {
- let status = result.status; // Process redirects
- if (redirectStatusCodes.has(status)) {
- let location = result.headers.get("Location");
- invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header"); // Support relative routing in internal redirects
- if (!ABSOLUTE_URL_REGEX.test(location)) {
- let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
- let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
- let resolvedLocation = resolveTo(location, routePathnames, new URL(request.url).pathname);
- invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + location); // Prepend the basename to the redirect location if we have one
- if (basename) {
- let path = resolvedLocation.pathname;
- resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
- }
- location = createPath(resolvedLocation);
- } else if (!isStaticRequest) {
- // Strip off the protocol+origin for same-origin absolute redirects.
- // If this is a static reques, we can let it go back to the browser
- // as-is
- let currentUrl = new URL(request.url);
- let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);
- if (url.origin === currentUrl.origin) {
- location = url.pathname + url.search + url.hash;
- }
- } // Don't process redirects in the router during static requests requests.
- // Instead, throw the Response and let the server handle it with an HTTP
- // redirect. We also update the Location header in place in this flow so
- // basename and relative routing is taken into account
- if (isStaticRequest) {
- result.headers.set("Location", location);
- throw result;
- }
- return {
- type: ResultType.redirect,
- status,
- location,
- revalidate: result.headers.get("X-Remix-Revalidate") !== null
- };
- } // For SSR single-route requests, we want to hand Responses back directly
- // without unwrapping. We do this with the QueryRouteResponse wrapper
- // interface so we can know whether it was returned or thrown
- if (isRouteRequest) {
- // eslint-disable-next-line no-throw-literal
- throw {
- type: resultType || ResultType.data,
- response: result
- };
- }
- let data;
- let contentType = result.headers.get("Content-Type"); // Check between word boundaries instead of startsWith() due to the last
- // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
- if (contentType && /\bapplication\/json\b/.test(contentType)) {
- data = await result.json();
- } else {
- data = await result.text();
- }
- if (resultType === ResultType.error) {
- return {
- type: resultType,
- error: new ErrorResponse(status, result.statusText, data),
- headers: result.headers
- };
- }
- return {
- type: ResultType.data,
- data,
- statusCode: result.status,
- headers: result.headers
- };
- }
- if (resultType === ResultType.error) {
- return {
- type: resultType,
- error: result
- };
- }
- if (result instanceof DeferredData) {
- return {
- type: ResultType.deferred,
- deferredData: result
- };
- }
- return {
- type: ResultType.data,
- data: result
- };
- } // Utility method for creating the Request instances for loaders/actions during
- // client-side navigations and fetches. During SSR we will always have a
- // Request instance from the static handler (query/queryRoute)
- function createClientSideRequest(history, location, signal, submission) {
- let url = history.createURL(stripHashFromPath(location)).toString();
- let init = {
- signal
- };
- if (submission && isMutationMethod(submission.formMethod)) {
- let {
- formMethod,
- formEncType,
- formData
- } = submission;
- init.method = formMethod.toUpperCase();
- init.body = formEncType === "application/x-www-form-urlencoded" ? convertFormDataToSearchParams(formData) : formData;
- } // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
- return new Request(url, init);
- }
- function convertFormDataToSearchParams(formData) {
- let searchParams = new URLSearchParams();
- for (let [key, value] of formData.entries()) {
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
- searchParams.append(key, value instanceof File ? value.name : value);
- }
- return searchParams;
- }
- function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {
- // Fill in loaderData/errors from our loaders
- let loaderData = {};
- let errors = null;
- let statusCode;
- let foundError = false;
- let loaderHeaders = {}; // Process loader results into state.loaderData/state.errors
- results.forEach((result, index) => {
- let id = matchesToLoad[index].route.id;
- invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData");
- if (isErrorResult(result)) {
- // Look upwards from the matched route for the closest ancestor
- // error boundary, defaulting to the root match
- let boundaryMatch = findNearestBoundary(matches, id);
- let error = result.error; // If we have a pending action error, we report it at the highest-route
- // that throws a loader error, and then clear it out to indicate that
- // it was consumed
- if (pendingError) {
- error = Object.values(pendingError)[0];
- pendingError = undefined;
- }
- errors = errors || {}; // Prefer higher error values if lower errors bubble to the same boundary
- if (errors[boundaryMatch.route.id] == null) {
- errors[boundaryMatch.route.id] = error;
- } // Clear our any prior loaderData for the throwing route
- loaderData[id] = undefined; // Once we find our first (highest) error, we set the status code and
- // prevent deeper status codes from overriding
- if (!foundError) {
- foundError = true;
- statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;
- }
- if (result.headers) {
- loaderHeaders[id] = result.headers;
- }
- } else {
- if (isDeferredResult(result)) {
- activeDeferreds.set(id, result.deferredData);
- loaderData[id] = result.deferredData.data;
- } else {
- loaderData[id] = result.data;
- } // Error status codes always override success status codes, but if all
- // loaders are successful we take the deepest status code.
- if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
- statusCode = result.statusCode;
- }
- if (result.headers) {
- loaderHeaders[id] = result.headers;
- }
- }
- }); // If we didn't consume the pending action error (i.e., all loaders
- // resolved), then consume it here. Also clear out any loaderData for the
- // throwing route
- if (pendingError) {
- errors = pendingError;
- loaderData[Object.keys(pendingError)[0]] = undefined;
- }
- return {
- loaderData,
- errors,
- statusCode: statusCode || 200,
- loaderHeaders
- };
- }
- function processLoaderData(state, matches, matchesToLoad, results, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds) {
- let {
- loaderData,
- errors
- } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds); // Process results from our revalidating fetchers
- for (let index = 0; index < revalidatingFetchers.length; index++) {
- let {
- key,
- match
- } = revalidatingFetchers[index];
- invariant(fetcherResults !== undefined && fetcherResults[index] !== undefined, "Did not find corresponding fetcher result");
- let result = fetcherResults[index]; // Process fetcher non-redirect errors
- if (isErrorResult(result)) {
- let boundaryMatch = findNearestBoundary(state.matches, match.route.id);
- if (!(errors && errors[boundaryMatch.route.id])) {
- errors = _extends({}, errors, {
- [boundaryMatch.route.id]: result.error
- });
- }
- state.fetchers.delete(key);
- } else if (isRedirectResult(result)) {
- // Should never get here, redirects should get processed above, but we
- // keep this to type narrow to a success result in the else
- invariant(false, "Unhandled fetcher revalidation redirect");
- } else if (isDeferredResult(result)) {
- // Should never get here, deferred data should be awaited for fetchers
- // in resolveDeferredResults
- invariant(false, "Unhandled fetcher deferred data");
- } else {
- let doneFetcher = {
- state: "idle",
- data: result.data,
- formMethod: undefined,
- formAction: undefined,
- formEncType: undefined,
- formData: undefined,
- " _hasFetcherDoneAnything ": true
- };
- state.fetchers.set(key, doneFetcher);
- }
- }
- return {
- loaderData,
- errors
- };
- }
- function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
- let mergedLoaderData = _extends({}, newLoaderData);
- for (let match of matches) {
- let id = match.route.id;
- if (newLoaderData.hasOwnProperty(id)) {
- if (newLoaderData[id] !== undefined) {
- mergedLoaderData[id] = newLoaderData[id];
- }
- } else if (loaderData[id] !== undefined) {
- mergedLoaderData[id] = loaderData[id];
- }
- if (errors && errors.hasOwnProperty(id)) {
- // Don't keep any loader data below the boundary
- break;
- }
- }
- return mergedLoaderData;
- } // Find the nearest error boundary, looking upwards from the leaf route (or the
- // route specified by routeId) for the closest ancestor error boundary,
- // defaulting to the root match
- function findNearestBoundary(matches, routeId) {
- let eligibleMatches = routeId ? matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1) : [...matches];
- return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];
- }
- function getShortCircuitMatches(routes) {
- // Prefer a root layout route if present, otherwise shim in a route object
- let route = routes.find(r => r.index || !r.path || r.path === "/") || {
- id: "__shim-error-route__"
- };
- return {
- matches: [{
- params: {},
- pathname: "",
- pathnameBase: "",
- route
- }],
- route
- };
- }
- function getInternalRouterError(status, _temp4) {
- let {
- pathname,
- routeId,
- method,
- type
- } = _temp4 === void 0 ? {} : _temp4;
- let statusText = "Unknown Server Error";
- let errorMessage = "Unknown @remix-run/router error";
- if (status === 400) {
- statusText = "Bad Request";
- if (method && pathname && routeId) {
- errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
- } else if (type === "defer-action") {
- errorMessage = "defer() is not supported in actions";
- }
- } else if (status === 403) {
- statusText = "Forbidden";
- errorMessage = "Route \"" + routeId + "\" does not match URL \"" + pathname + "\"";
- } else if (status === 404) {
- statusText = "Not Found";
- errorMessage = "No route matches URL \"" + pathname + "\"";
- } else if (status === 405) {
- statusText = "Method Not Allowed";
- if (method && pathname && routeId) {
- errorMessage = "You made a " + method.toUpperCase() + " request to \"" + pathname + "\" but " + ("did not provide an `action` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
- } else if (method) {
- errorMessage = "Invalid request method \"" + method.toUpperCase() + "\"";
- }
- }
- return new ErrorResponse(status || 500, statusText, new Error(errorMessage), true);
- } // Find any returned redirect errors, starting from the lowest match
- function findRedirect(results) {
- for (let i = results.length - 1; i >= 0; i--) {
- let result = results[i];
- if (isRedirectResult(result)) {
- return result;
- }
- }
- }
- function stripHashFromPath(path) {
- let parsedPath = typeof path === "string" ? parsePath(path) : path;
- return createPath(_extends({}, parsedPath, {
- hash: ""
- }));
- }
- function isHashChangeOnly(a, b) {
- return a.pathname === b.pathname && a.search === b.search && a.hash !== b.hash;
- }
- function isDeferredResult(result) {
- return result.type === ResultType.deferred;
- }
- function isErrorResult(result) {
- return result.type === ResultType.error;
- }
- function isRedirectResult(result) {
- return (result && result.type) === ResultType.redirect;
- }
- function isResponse(value) {
- return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined";
- }
- function isRedirectResponse(result) {
- if (!isResponse(result)) {
- return false;
- }
- let status = result.status;
- let location = result.headers.get("Location");
- return status >= 300 && status <= 399 && location != null;
- }
- function isQueryRouteResponse(obj) {
- return obj && isResponse(obj.response) && (obj.type === ResultType.data || ResultType.error);
- }
- function isValidMethod(method) {
- return validRequestMethods.has(method);
- }
- function isMutationMethod(method) {
- return validMutationMethods.has(method);
- }
- async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {
- for (let index = 0; index < results.length; index++) {
- let result = results[index];
- let match = matchesToLoad[index];
- let currentMatch = currentMatches.find(m => m.route.id === match.route.id);
- let isRevalidatingLoader = currentMatch != null && !isNewRouteInstance(currentMatch, match) && (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
- if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {
- // Note: we do not have to touch activeDeferreds here since we race them
- // against the signal in resolveDeferredData and they'll get aborted
- // there if needed
- await resolveDeferredData(result, signal, isFetcher).then(result => {
- if (result) {
- results[index] = result || results[index];
- }
- });
- }
- }
- }
- async function resolveDeferredData(result, signal, unwrap) {
- if (unwrap === void 0) {
- unwrap = false;
- }
- let aborted = await result.deferredData.resolveData(signal);
- if (aborted) {
- return;
- }
- if (unwrap) {
- try {
- return {
- type: ResultType.data,
- data: result.deferredData.unwrappedData
- };
- } catch (e) {
- // Handle any TrackedPromise._error values encountered while unwrapping
- return {
- type: ResultType.error,
- error: e
- };
- }
- }
- return {
- type: ResultType.data,
- data: result.deferredData.data
- };
- }
- function hasNakedIndexQuery(search) {
- return new URLSearchParams(search).getAll("index").some(v => v === "");
- } // Note: This should match the format exported by useMatches, so if you change
- // this please also change that :) Eventually we'll DRY this up
- function createUseMatchesMatch(match, loaderData) {
- let {
- route,
- pathname,
- params
- } = match;
- return {
- id: route.id,
- pathname,
- params,
- data: loaderData[route.id],
- handle: route.handle
- };
- }
- function getTargetMatch(matches, location) {
- let search = typeof location === "string" ? parsePath(location).search : location.search;
- if (matches[matches.length - 1].route.index && hasNakedIndexQuery(search || "")) {
- // Return the leaf index route when index is present
- return matches[matches.length - 1];
- } // Otherwise grab the deepest "path contributing" match (ignoring index and
- // pathless layout routes)
- let pathMatches = getPathContributingMatches(matches);
- return pathMatches[pathMatches.length - 1];
- } //#endregion
- exports.AbortedDeferredError = AbortedDeferredError;
- exports.ErrorResponse = ErrorResponse;
- exports.IDLE_BLOCKER = IDLE_BLOCKER;
- exports.IDLE_FETCHER = IDLE_FETCHER;
- exports.IDLE_NAVIGATION = IDLE_NAVIGATION;
- exports.UNSAFE_DEFERRED_SYMBOL = UNSAFE_DEFERRED_SYMBOL;
- exports.UNSAFE_DeferredData = DeferredData;
- exports.UNSAFE_convertRoutesToDataRoutes = convertRoutesToDataRoutes;
- exports.UNSAFE_getPathContributingMatches = getPathContributingMatches;
- exports.createBrowserHistory = createBrowserHistory;
- exports.createHashHistory = createHashHistory;
- exports.createMemoryHistory = createMemoryHistory;
- exports.createPath = createPath;
- exports.createRouter = createRouter;
- exports.createStaticHandler = createStaticHandler;
- exports.defer = defer;
- exports.generatePath = generatePath;
- exports.getStaticContextFromError = getStaticContextFromError;
- exports.getToPathname = getToPathname;
- exports.invariant = invariant;
- exports.isRouteErrorResponse = isRouteErrorResponse;
- exports.joinPaths = joinPaths;
- exports.json = json;
- exports.matchPath = matchPath;
- exports.matchRoutes = matchRoutes;
- exports.normalizePathname = normalizePathname;
- exports.parsePath = parsePath;
- exports.redirect = redirect;
- exports.resolvePath = resolvePath;
- exports.resolveTo = resolveTo;
- exports.stripBasename = stripBasename;
- exports.warning = warning;
- //# sourceMappingURL=router.cjs.js.map
|