what-router 0.11.8 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.min.js +3 -3
- package/index.d.ts +2 -2
- package/package.json +2 -2
- package/src/index.js +54 -5
package/dist/index.min.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{signal as
|
|
1
|
+
import{signal as x,effect as I,computed as b,batch as M,h as c,ErrorBoundary as X}from"what-core";function O(e){let t=e.replace(/\([\w-]+\)\//g,"").replace(/\[\.\.\.(\w+)\]/g,(i,u)=>`*:${u}`).replace(/\[(\w+)\]/g,":$1"),n=[],r=null,o=t.split("/").map(i=>i.startsWith("*:")?(r=i.slice(2),n.push(r),"(.+)"):i==="*"?(r="rest",n.push("rest"),"(.+)"):i.startsWith(":")?(n.push(i.slice(1)),"([^/]+)"):i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("/");return{regex:new RegExp(`^${o}$`),paramNames:n,catchAll:r}}var _=new Map,F=1e3;function H(e){let t=_.get(e);return t||(t=O(e),_.size>=F&&_.clear(),_.set(e,t)),t}function K(e,t){let n;try{n=decodeURIComponent(e)}catch{return null}let r=n.split(/[/\\]/);return!t&&r.length>1||r.includes("..")?null:n}function A(e,t){let n=t.filter(r=>r.path);n.sort((r,o)=>{let a=(r.path.match(/:/g)||[]).length+(r.path.includes("*")?100:0),i=(o.path.match(/:/g)||[]).length+(o.path.includes("*")?100:0);return a-i});for(let r of n){let{regex:o,paramNames:a,catchAll:i}=H(r.path),u=e.match(o);if(u){let p={},l=!1;if(a.forEach((s,d)=>{let g=K(u[d+1],s===i);if(g===null){l=!0;return}p[s]=g}),l)continue;return{route:r,params:p}}}return null}function C(e){let t=Object.create(null);if(!e)return t;let n=e.startsWith("?")?e.slice(1):e;for(let r of n.split("&")){let[o,a]=r.split("=");if(!o)continue;let i=decodeURIComponent(o),u=a?decodeURIComponent(a):"";Object.prototype.hasOwnProperty.call(t,i)?Array.isArray(t[i])?t[i].push(u):t[i]=[t[i],u]:t[i]=u}return t}var Q=new Set(["http:","https:","mailto:","tel:"]);function k(e){if(typeof e!="string")return!1;let n=e.trim().replace(/[\s\x00-\x1f]/g,"").toLowerCase();if(n.startsWith("javascript:")||n.startsWith("data:")||n.startsWith("vbscript:")||/^[/\\]{2}/.test(n))return!1;let r=n.match(/^([a-z][a-z0-9+.-]*:)/);return r?Q.has(r[1]):!n.includes("\\")}var h=x(typeof location<"u"?location.pathname+location.search+location.hash:"/"),G=x({}),D=x({}),y=x(!1),R=x(null),V=x(null),m={get url(){return h()},get path(){return h().split("?")[0].split("#")[0]},get params(){return G()},get query(){return D()},get hash(){let e=h().split("#")[1];return e?"#"+e:""},get isNavigating(){return y()},get error(){return V()}},T=[],q=[];function z(e,t){return e.push(t),()=>{let n=e.indexOf(t);n!==-1&&e.splice(n,1)}}function ue(e){return z(T,e)}function le(e){return z(q,e)}async function v(e,t={}){let{replace:n=!1,state:r=null,transition:o=!0,_fromPopstate:a=!1,_redirectChain:i=!1}=t;if(i||(w.length=0),!k(e)){typeof console<"u"&&console.warn("[what-router] Blocked navigation to unsafe URL:",e);return}if(typeof window<"u"&&e.startsWith("#")){let d=h().split("#")[0]+e;history.replaceState(r,"",d),h.set(d);let g=document.querySelector(e);g&&g.scrollIntoView({behavior:"smooth"});return}if(e===h()||y.peek())return;y.set(!0),R.set(e);let u=h();if(T.length){let l=!1;try{for(let s of T.slice())if(await s(e,u)===!1){l=!0;break}}catch(s){throw y.set(!1),R.set(null),s}if(l){y.set(!1),R.set(null),a&&typeof history<"u"&&history.pushState(null,"",u);return}}V.set(null);let p=()=>{a||(typeof window<"u"&&E.set(h(),{x:window.scrollX,y:window.scrollY}),n?history.replaceState(r,"",e):history.pushState(r,"",e)),h.set(e),y.set(!1),R.set(null)};if(o&&typeof document<"u"&&document.startViewTransition)try{await document.startViewTransition(p).finished}catch{}else p();if(q.length)for(let l of q.slice())l(e,u)}var B=Symbol.for("what.router.redirect"),Y=Symbol.for("what.navigation.signal");function fe(e,t={}){if(!k(e)){let r=typeof e=="string"?e:Object.prototype.toString.call(e),o=new Error(`[what-router] redirect() refused an unsafe target: ${r}`);throw o.code="ERR_UNSAFE_REDIRECT",o.suggestion='redirect() accepts same-origin paths and http:, https:, mailto: or tel: URLs only. Protocol-relative ("//host"), backslash-smuggled and javascript:/data: targets are open-redirect vectors. Check a user-supplied target against an allowlist first.',o.codeExample=`// Bad - a user-controlled target can leave your origin:
|
|
2
2
|
redirect(query.next);
|
|
3
3
|
|
|
4
4
|
// Good - allowlist the target first:
|
|
5
|
-
redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let
|
|
5
|
+
redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let n=new Error(`[what-router] redirect to ${e}`);throw n.name="RouterRedirect",n.code="ERR_REDIRECT_NOT_CAUGHT",n.suggestion="Seeing this signal in your console means nothing caught it. redirect() works from route middleware and from a component body, where the Router catches it. From an event handler, a promise callback or a timer, call navigate(to) instead. A try/catch around the redirect() call also swallows it.",n.codeExample=`// Bad - an event handler runs long after the render the Router caught:
|
|
6
6
|
<button onclick={() => redirect('/login')}>Sign in</button>
|
|
7
7
|
|
|
8
8
|
// Good - navigate() from a handler:
|
|
@@ -12,4 +12,4 @@ redirect(ALLOWED.has(query.next) ? query.next : '/');`,o}let r=new Error(`[what-
|
|
|
12
12
|
function Private() {
|
|
13
13
|
if (!user()) redirect('/login');
|
|
14
14
|
return <Secret />;
|
|
15
|
-
}`,
|
|
15
|
+
}`,n[B]=!0,n[Y]=()=>{L(e,t)},n.to=e,n.options=t,n}typeof window<"u"&&window.addEventListener("popstate",()=>{E.set(h(),{x:window.scrollX,y:window.scrollY});let e=location.pathname+location.search+location.hash;v(e,{replace:!0,_fromPopstate:!0,transition:!1}).then(()=>{let t=E.get(e);t&&requestAnimationFrame(()=>window.scrollTo(t.x,t.y))})});function J(e,t){let n=[];if(!e.path)return n;let r=e.path.split("/").filter(Boolean),o="";for(let a of r){o+="/"+a;let i=t.find(u=>u.layout&&u.path===o+"/_layout");i&&n.push(i.layout)}return e.layout&&n.push(e.layout),n}var w=[],Z=10;function W(e){return c("div",{class:"what-redirect-loop"},c("h1",null,"Redirect Loop"),c("p",null,e))}function L(e,t){if(w.push(e),w.length>Z){let o=w.slice(-5).join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect loop detected: ${o}`),y.set(!1),R.set(null),W("Too many redirects. Check your middleware configuration.")}let n=new Set,r=!1;for(let o of w){if(n.has(o)){r=!0;break}n.add(o)}if(r){let o=w.join(" \u2192 ");return w.length=0,console.error(`[what-router] Redirect cycle detected: ${o}`),y.set(!1),R.set(null),W("Circular redirect detected. Check your middleware configuration.")}return v(e,{replace:!0,...t,_redirectChain:!0}),null}function ee(e,t,n){if(!e)return null;let r=e.split("?")[0].split("#")[0],o=A(r,t);return!o||o.route===n?null:o.route.loading||null}function te({routes:e,fallback:t,globalLayout:n}){let r=()=>{let a=h(),i=a.split("?")[0].split("#")[0],u=a.split("?")[1]?.split("#")[0]||"",p=y(),l=A(i,e);if(l){M(()=>{G.set(l.params),D.set(C(u))});let{route:s,params:d}=l,g=C(u);if(s.middleware&&s.middleware.length>0)for(let P of s.middleware){let N=P({path:i,params:d,query:g,route:s});if(N===!1)return t?c(t,{}):c("div",{class:"what-403"},c("h1",null,"403"),c("p",null,"Access denied"));if(typeof N=="string")return L(N)}let f,S=p?ee(R(),e,s):null;S?f=c(S,{}):s.loading&&p?f=c(s.loading,{}):f=c(s.component,{params:d,query:g,route:s}),s.error&&(f=c(X,{fallback:s.error},f));let j=J(s,e);for(let P of j.reverse())f=c(P,{params:d,query:g},f);return f}return t?c(t,{}):c("div",{class:"what-404"},c("h1",null,"404"),c("p",null,"Page not found"))},o=()=>{let a=null;try{return r()}catch(i){if(!i||!i[B])throw i;a=i}return L(a.to,a.options)};return n?c(n,{},o):o}function ne({href:e,class:t,className:n,children:r,replace:o,prefetch:a=!0,activeClass:i="active",exactActiveClass:u="exact-active",transition:p=!0,...l}){let s=k(e)?e:"about:blank";!k(e)&&typeof console<"u"&&console.warn("[what-router] Link blocked unsafe href:",e);let d=s.split("?")[0].split("#")[0];return c("a",{href:s,class:()=>{let f=m.path,S=d==="/"?f==="/":f===d||f.startsWith(d+"/");return[t||n,S&&i,f===d&&u].filter(Boolean).join(" ")||void 0},onclick:f=>{f.ctrlKey||f.metaKey||f.shiftKey||f.altKey||f.button!==0||(f.preventDefault(),v(s,{replace:o,transition:p}))},onmouseenter:a?()=>U(s):void 0,...l},...Array.isArray(r)?r:[r])}function de(e){return ne(e)}function pe(e){return Object.entries(e).map(([t,n])=>typeof n=="function"?{path:t,component:n}:{path:t,...n})}function re(e,t){let n=e.endsWith("/")?e.slice(0,-1):e,r=!t||t==="/"?"/":t.startsWith("/")?t:`/${t}`;return r==="/"?n||"/":n+r}function he(e,t,n={}){let{layout:r,loading:o,error:a}=n;return t.map(i=>({...i,path:re(e,i.path),layout:i.layout||r,loading:i.loading||o,error:i.error||a}))}function me(e,t,n={}){let{layout:r,middleware:o}=n;return t.map(a=>({...a,_group:e,layout:a.layout||r,middleware:[...a.middleware||[],...o||[]]}))}function ge({to:e}){return v(e,{replace:!0}),null}function ye(e,t){return n=>function(o){let a=e(o);return a instanceof Promise?c("div",{class:"what-guard-loading"},"Loading..."):a?c(n,o):typeof t=="string"?(v(t,{replace:!0}),null):c(t,o)}}function we(e,t={}){let{fallback:n="/login",loading:r=null}=t;return o=>function(i){let u=x("pending"),p=x(null),l=!1;return I(()=>(l=!1,Promise.resolve(e(i)).then(s=>{l||(p.set(s),u.set(s?"allowed":"denied"))}).catch(()=>{l||u.set("denied")}),()=>{l=!0})),()=>{let s=u();return s==="pending"?r?c(r,{}):null:s==="allowed"?c(o,i):typeof n=="string"?(v(n,{replace:!0}),null):c(n,i)}}}var $=new Set;function U(e){if(typeof document>"u"||$.has(e))return;$.add(e);let t=document.createElement("link");t.rel="prefetch",t.href=e,document.head.appendChild(t)}var E=new Map;function xe(){typeof window>"u"||(window.addEventListener("beforeunload",()=>{E.set(location.pathname,window.scrollY)}),I(()=>{let e=m.path,t=E.get(e);requestAnimationFrame(()=>{t!==void 0?window.scrollTo(0,t):m.hash?document.querySelector(m.hash)?.scrollIntoView():window.scrollTo(0,0)})}))}function ve(e){return{style:{viewTransitionName:e}}}function Re(e){typeof document>"u"||(document.documentElement.dataset.transition=e)}function be(){return{path:b(()=>m.path),params:b(()=>m.params),query:b(()=>m.query),hash:b(()=>m.hash),isNavigating:b(()=>m.isNavigating),navigate:v,prefetch:U}}function Ee(){return m.params}function Se(){return m.query}function _e(){return v}function Ae(e){U(e)}function Ce({children:e}){return e||null}function ke({routes:e,layout:t,fallback:n,error:r}){let o=e.map(a=>({path:a.path,component:a.component,layout:a.layout||void 0,loading:a.loading||void 0,error:a.error||r||void 0,_mode:a.mode||"client"}));return te({routes:o,globalLayout:t,fallback:n||oe})}function oe(){return c("div",{style:"text-align:center;padding:60px 20px"},c("h1",{style:"font-size:48px;margin-bottom:8px"},"404"),c("p",{style:"color:#64748b"},"Page not found"))}export{ke as FileRouter,ne as Link,de as NavLink,Ce as Outlet,ge as Redirect,te as Router,le as afterNavigate,we as asyncGuard,ue as beforeNavigate,O as compilePath,pe as defineRoutes,xe as enableScrollRestoration,ye as guard,k as isSafeUrl,A as matchRoute,v as navigate,he as nestedRoutes,C as parseQuery,U as prefetch,Ae as prefetchRoute,fe as redirect,m as route,me as routeGroup,Re as setViewTransition,_e as useNavigate,Ee as useParams,be as useRoute,Se as useSearch,ve as viewTransitionName};
|
package/index.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface RouteConfig {
|
|
|
49
49
|
/** Loading component */
|
|
50
50
|
loading?: Component<{}>;
|
|
51
51
|
/** Error component */
|
|
52
|
-
error?: Component<{ error: Error }>;
|
|
52
|
+
error?: Component<{ error: Error; reset: () => void }>;
|
|
53
53
|
/** Route middleware */
|
|
54
54
|
middleware?: RouteMiddleware[];
|
|
55
55
|
}
|
|
@@ -91,7 +91,7 @@ export interface FileRouterProps {
|
|
|
91
91
|
routes: FileRouteConfig[];
|
|
92
92
|
layout?: Component<{ children?: VNodeChild }>;
|
|
93
93
|
fallback?: Component<{}>;
|
|
94
|
-
error?: Component<{ error: Error }>;
|
|
94
|
+
error?: Component<{ error: Error; reset: () => void }>;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** Router driven by what-compiler's generated route manifest (virtual:what-routes). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "What Framework - File-based & programmatic router with View Transitions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"dependencies": {},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"what-core": "^0.
|
|
38
|
+
"what-core": "^0.12.1"
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
package/src/index.js
CHANGED
|
@@ -45,6 +45,12 @@ const _url = signal(typeof location !== 'undefined' ? location.pathname + locati
|
|
|
45
45
|
const _params = signal({});
|
|
46
46
|
const _query = signal({});
|
|
47
47
|
const _isNavigating = signal(false);
|
|
48
|
+
// The URL a navigation is heading TO, live for the duration of the navigation.
|
|
49
|
+
// _url only commits at the very end, so during an awaited guard the router still
|
|
50
|
+
// matches the route being LEFT. Without this, a destination route's `loading:`
|
|
51
|
+
// component could never be shown: the only `loading:` in scope belonged to the
|
|
52
|
+
// page the user was leaving.
|
|
53
|
+
const _pendingUrl = signal(null);
|
|
48
54
|
const _navigationError = signal(null);
|
|
49
55
|
|
|
50
56
|
export const route = {
|
|
@@ -127,6 +133,7 @@ export async function navigate(to, opts = {}) {
|
|
|
127
133
|
// beforeNavigate hook otherwise leaves a gap two navigations both get through.
|
|
128
134
|
if (_isNavigating.peek()) return;
|
|
129
135
|
_isNavigating.set(true);
|
|
136
|
+
_pendingUrl.set(to);
|
|
130
137
|
|
|
131
138
|
const from = _url();
|
|
132
139
|
|
|
@@ -143,10 +150,12 @@ export async function navigate(to, opts = {}) {
|
|
|
143
150
|
} catch (e) {
|
|
144
151
|
// A throwing guard must not leave the router permanently wedged.
|
|
145
152
|
_isNavigating.set(false);
|
|
153
|
+
_pendingUrl.set(null);
|
|
146
154
|
throw e;
|
|
147
155
|
}
|
|
148
156
|
if (cancelled) {
|
|
149
157
|
_isNavigating.set(false);
|
|
158
|
+
_pendingUrl.set(null);
|
|
150
159
|
if (_fromPopstate && typeof history !== 'undefined') history.pushState(null, '', from);
|
|
151
160
|
return;
|
|
152
161
|
}
|
|
@@ -159,7 +168,7 @@ export async function navigate(to, opts = {}) {
|
|
|
159
168
|
if (!_fromPopstate) {
|
|
160
169
|
// Save scroll position for current URL before navigating away
|
|
161
170
|
if (typeof window !== 'undefined') {
|
|
162
|
-
scrollPositions.set(_url(), { x: scrollX, y: scrollY });
|
|
171
|
+
scrollPositions.set(_url(), { x: window.scrollX, y: window.scrollY });
|
|
163
172
|
}
|
|
164
173
|
if (replace) {
|
|
165
174
|
history.replaceState(state, '', to);
|
|
@@ -169,6 +178,7 @@ export async function navigate(to, opts = {}) {
|
|
|
169
178
|
}
|
|
170
179
|
_url.set(to);
|
|
171
180
|
_isNavigating.set(false);
|
|
181
|
+
_pendingUrl.set(null);
|
|
172
182
|
};
|
|
173
183
|
|
|
174
184
|
// Use View Transitions API if available and enabled
|
|
@@ -245,7 +255,7 @@ function Private() {
|
|
|
245
255
|
if (typeof window !== 'undefined') {
|
|
246
256
|
window.addEventListener('popstate', () => {
|
|
247
257
|
// Save scroll position for the URL we're leaving
|
|
248
|
-
scrollPositions.set(_url(), { x: scrollX, y: scrollY });
|
|
258
|
+
scrollPositions.set(_url(), { x: window.scrollX, y: window.scrollY });
|
|
249
259
|
|
|
250
260
|
const newUrl = location.pathname + location.search + location.hash;
|
|
251
261
|
// Use _fromPopstate flag so navigate() skips pushState (browser already updated URL)
|
|
@@ -325,6 +335,7 @@ function handleRedirect(target, options) {
|
|
|
325
335
|
_redirectHistory.length = 0;
|
|
326
336
|
console.error(`[what-router] Redirect loop detected: ${cycle}`);
|
|
327
337
|
_isNavigating.set(false);
|
|
338
|
+
_pendingUrl.set(null);
|
|
328
339
|
return loopScreen('Too many redirects. Check your middleware configuration.');
|
|
329
340
|
}
|
|
330
341
|
|
|
@@ -339,6 +350,7 @@ function handleRedirect(target, options) {
|
|
|
339
350
|
_redirectHistory.length = 0;
|
|
340
351
|
console.error(`[what-router] Redirect cycle detected: ${cycle}`);
|
|
341
352
|
_isNavigating.set(false);
|
|
353
|
+
_pendingUrl.set(null);
|
|
342
354
|
return loopScreen('Circular redirect detected. Check your middleware configuration.');
|
|
343
355
|
}
|
|
344
356
|
|
|
@@ -348,6 +360,17 @@ function handleRedirect(target, options) {
|
|
|
348
360
|
|
|
349
361
|
// --- Router Component ---
|
|
350
362
|
|
|
363
|
+
// The `loading:` component of the route a navigation is heading to, or null when
|
|
364
|
+
// there is no pending destination, it does not match, it declares no loading
|
|
365
|
+
// component, or it is the route already being rendered.
|
|
366
|
+
function destinationLoading(pendingUrl, routes, currentRoute) {
|
|
367
|
+
if (!pendingUrl) return null;
|
|
368
|
+
const destPath = pendingUrl.split('?')[0].split('#')[0];
|
|
369
|
+
const dest = matchRoute(destPath, routes);
|
|
370
|
+
if (!dest || dest.route === currentRoute) return null;
|
|
371
|
+
return dest.route.loading || null;
|
|
372
|
+
}
|
|
373
|
+
|
|
351
374
|
export function Router({ routes, fallback, globalLayout }) {
|
|
352
375
|
// The Router component runs ONCE. `content` is a reactive function child that
|
|
353
376
|
// re-evaluates whenever _url changes; the fine-grained runtime reconciles only
|
|
@@ -389,7 +412,17 @@ export function Router({ routes, fallback, globalLayout }) {
|
|
|
389
412
|
// Build element with loading state support
|
|
390
413
|
let element;
|
|
391
414
|
|
|
392
|
-
|
|
415
|
+
// While a navigation is in flight, a `loading:` declared by the
|
|
416
|
+
// DESTINATION wins. _url has not committed yet, so `r` is still the route
|
|
417
|
+
// being left: without this, declaring `loading:` on the page you are
|
|
418
|
+
// navigating TO could never show it. Falling back to the departing route's
|
|
419
|
+
// `loading:` preserves the existing behaviour for the case where only the
|
|
420
|
+
// page being left declares one.
|
|
421
|
+
const pendingLoading = isNavigating ? destinationLoading(_pendingUrl(), routes, r) : null;
|
|
422
|
+
|
|
423
|
+
if (pendingLoading) {
|
|
424
|
+
element = h(pendingLoading, {});
|
|
425
|
+
} else if (r.loading && isNavigating) {
|
|
393
426
|
element = h(r.loading, {});
|
|
394
427
|
} else {
|
|
395
428
|
element = h(r.component, {
|
|
@@ -521,12 +554,23 @@ export function defineRoutes(config) {
|
|
|
521
554
|
|
|
522
555
|
// --- Nested Route Helper ---
|
|
523
556
|
|
|
557
|
+
// Join a base path with a child path. Naive concatenation turned the index
|
|
558
|
+
// child of `nestedRoutes('/dashboard', [{ path: '/' }, ...])` — the form the
|
|
559
|
+
// README documents — into '/dashboard/', which '/dashboard' does not match, so
|
|
560
|
+
// the documented example 404s on its own index route.
|
|
561
|
+
function joinRoutePath(basePath, childPath) {
|
|
562
|
+
const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
563
|
+
const child = !childPath || childPath === '/' ? '/' : (childPath.startsWith('/') ? childPath : `/${childPath}`);
|
|
564
|
+
if (child === '/') return base || '/';
|
|
565
|
+
return base + child;
|
|
566
|
+
}
|
|
567
|
+
|
|
524
568
|
export function nestedRoutes(basePath, children, options = {}) {
|
|
525
569
|
const { layout, loading, error } = options;
|
|
526
570
|
|
|
527
571
|
return children.map(child => ({
|
|
528
572
|
...child,
|
|
529
|
-
path: basePath
|
|
573
|
+
path: joinRoutePath(basePath, child.path),
|
|
530
574
|
layout: child.layout || layout,
|
|
531
575
|
loading: child.loading || loading,
|
|
532
576
|
error: child.error || error,
|
|
@@ -743,11 +787,16 @@ export function FileRouter({
|
|
|
743
787
|
fallback,
|
|
744
788
|
error: globalError,
|
|
745
789
|
}) {
|
|
746
|
-
// Convert file-router route format to Router's expected format
|
|
790
|
+
// Convert file-router route format to Router's expected format.
|
|
791
|
+
// loading/error used to be dropped here, so a route could declare either and
|
|
792
|
+
// never see it rendered, and the `error` prop was destructured and then never
|
|
793
|
+
// read: a documented public prop that silently did nothing.
|
|
747
794
|
const routerRoutes = routes.map(r => ({
|
|
748
795
|
path: r.path,
|
|
749
796
|
component: r.component,
|
|
750
797
|
layout: r.layout || undefined,
|
|
798
|
+
loading: r.loading || undefined,
|
|
799
|
+
error: r.error || globalError || undefined,
|
|
751
800
|
// Attach page mode as metadata for build system
|
|
752
801
|
_mode: r.mode || 'client',
|
|
753
802
|
}));
|