stx-router 0.2.527 → 0.2.529
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/client.js +165 -16
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -55,7 +55,7 @@ function routerSource() {
|
|
|
55
55
|
if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
|
|
56
56
|
|
|
57
57
|
// \u2500\u2500 Configuration \u2500\u2500
|
|
58
|
-
var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false,cssLoadTimeout:1500,scrollRestoration:true,screens:'auto',screenLimit:8,navDuration:350,swipeBack:true,swipeEdge:20,prefetchVisible:true,prefetchVisibleMax:12,revalidate:true,revalidateAfter:3000};
|
|
58
|
+
var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false,cssLoadTimeout:1500,scrollRestoration:true,screens:'auto',screenLimit:8,navDuration:350,swipeBack:true,swipeEdge:20,prefetchVisible:true,prefetchVisibleMax:12,revalidate:true,revalidateAfter:3000,keepOpening:true};
|
|
59
59
|
var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
|
|
60
60
|
var containerSel=o.container;
|
|
61
61
|
var debug=!!o.debug;
|
|
@@ -135,8 +135,14 @@ function routerSource() {
|
|
|
135
135
|
function rememberScroll(){
|
|
136
136
|
if(active&&active.el&&!active.el.hasAttribute(HIDDEN))saveScroll(active);
|
|
137
137
|
if(!restoreScroll)return;
|
|
138
|
+
// The window's position as the swap began, like the scrollers' below.
|
|
139
|
+
// Read now it is wrong: the outgoing page has been replaced by one still
|
|
140
|
+
// filling in, a few hundred pixels tall, and the window has clamped to it
|
|
141
|
+
// -- every entry was filed at 0,0 and Back went to the top.
|
|
142
|
+
var win=winSnap||[window.pageXOffset||window.scrollX||0,window.pageYOffset||window.scrollY||0];
|
|
143
|
+
winSnap=undefined;
|
|
138
144
|
try{
|
|
139
|
-
window.sessionStorage.setItem(SCROLL_STORE+scrollToken,
|
|
145
|
+
window.sessionStorage.setItem(SCROLL_STORE+scrollToken,win[0]+','+win[1]);
|
|
140
146
|
// Taken as the swap began when there is one: by the time the history
|
|
141
147
|
// is written the outgoing page's scrollers have been replaced.
|
|
142
148
|
var inner=innerSnap!==undefined?innerSnap:innerScroll(activeRoot());
|
|
@@ -147,16 +153,76 @@ function routerSource() {
|
|
|
147
153
|
}
|
|
148
154
|
var pendingInner=null;
|
|
149
155
|
var innerSnap;
|
|
156
|
+
var winSnap;
|
|
157
|
+
function snapScroll(){
|
|
158
|
+
innerSnap=innerScroll(activeRoot());
|
|
159
|
+
winSnap=[window.pageXOffset||window.scrollX||0,window.pageYOffset||window.scrollY||0];
|
|
160
|
+
}
|
|
150
161
|
function readInner(token){
|
|
151
162
|
try{return JSON.parse(window.sessionStorage.getItem(SCROLL_STORE+token+':i')||'null')}catch(e){return null}
|
|
152
163
|
}
|
|
153
164
|
// One place decides where a swap leaves the viewport, so the fragment path
|
|
154
165
|
// and the whole-document path cannot drift apart.
|
|
166
|
+
// A page put back by Back is often shorter when it is swapped in than when
|
|
167
|
+
// it was left: its lists are drawn from data that is still on its way (or
|
|
168
|
+
// from a store a moment later), so scrollTo clamps to the little there is
|
|
169
|
+
// and the place is lost -- Back always landed at the top of a long page.
|
|
170
|
+
// For a moment after, hold the position: try again each time the page
|
|
171
|
+
// changes size. The person scrolling, a key, a tap, the page scrolling
|
|
172
|
+
// itself, the next navigation or four seconds (a page whose data is slow
|
|
173
|
+
// to come) end the hold.
|
|
174
|
+
var scrollHold=null;
|
|
175
|
+
var HOLD_ENDERS=['wheel','touchstart','keydown','pointerdown'];
|
|
176
|
+
function releaseScrollHold(){
|
|
177
|
+
if(!scrollHold)return;
|
|
178
|
+
var h=scrollHold;scrollHold=null;
|
|
179
|
+
if(h.ro)h.ro.disconnect();
|
|
180
|
+
clearTimeout(h.timer);
|
|
181
|
+
// The window and root the hold was made on: by the time its timer fires
|
|
182
|
+
// the document may be another one.
|
|
183
|
+
if(h.onScroll)h.win.removeEventListener('scroll',h.onScroll);
|
|
184
|
+
if(h.anchor!==null&&h.root)h.root.style.overflowAnchor=h.anchor;
|
|
185
|
+
HOLD_ENDERS.forEach(function(n){h.win.removeEventListener(n,h.stop,true)});
|
|
186
|
+
}
|
|
187
|
+
function holdScroll(x,y){
|
|
188
|
+
releaseScrollHold();
|
|
189
|
+
var reached=function(){return Math.abs((window.pageYOffset||window.scrollY||0)-y)<2};
|
|
190
|
+
if(typeof ResizeObserver!=='function')return;
|
|
191
|
+
var h={stop:releaseScrollHold,ro:null,timer:0,onScroll:null,anchor:null,win:window,root:document.documentElement};
|
|
192
|
+
// Scroll anchoring off while held: content filling in above the place
|
|
193
|
+
// would otherwise carry the window down with it (Chrome keeps what is
|
|
194
|
+
// on screen in view), and Back landed thousands of pixels past it.
|
|
195
|
+
var rootStyle=document.documentElement&&document.documentElement.style;
|
|
196
|
+
if(rootStyle){h.anchor=rootStyle.overflowAnchor||'';rootStyle.overflowAnchor='none'}
|
|
197
|
+
h.ro=new ResizeObserver(function(){
|
|
198
|
+
// Held for the whole window even once reached: a page that is tall
|
|
199
|
+
// enough when it is put back is often still drawing, above the place
|
|
200
|
+
// as much as below it.
|
|
201
|
+
if(scrollHold!==h||reached())return;
|
|
202
|
+
window.scrollTo({left:x,top:y,behavior:'instant'});
|
|
203
|
+
});
|
|
204
|
+
h.ro.observe(document.body||document.documentElement);
|
|
205
|
+
h.timer=setTimeout(releaseScrollHold,4000);
|
|
206
|
+
HOLD_ENDERS.forEach(function(n){window.addEventListener(n,h.stop,{capture:true,passive:true})});
|
|
207
|
+
// Moved by something else (a scrollbar dragged, the page scrolling to
|
|
208
|
+
// what it wants to show): theirs wins. Only the clamp to a page still
|
|
209
|
+
// too short, at its very end, is the hold's own business.
|
|
210
|
+
h.onScroll=function(){
|
|
211
|
+
var at=window.pageYOffset||window.scrollY||0;
|
|
212
|
+
var end=Math.max(0,(document.documentElement.scrollHeight||0)-(window.innerHeight||0));
|
|
213
|
+
if(Math.abs(at-y)<2||(at<y&&at>=end-1))return;
|
|
214
|
+
releaseScrollHold();
|
|
215
|
+
};
|
|
216
|
+
window.addEventListener('scroll',h.onScroll,{passive:true});
|
|
217
|
+
scrollHold=h;
|
|
218
|
+
}
|
|
155
219
|
function applyScroll(hash){
|
|
220
|
+
releaseScrollHold();
|
|
156
221
|
if(pendingScroll){
|
|
157
222
|
var at=pendingScroll;
|
|
158
223
|
pendingScroll=null;
|
|
159
224
|
window.scrollTo({left:at[0],top:at[1],behavior:'instant'});
|
|
225
|
+
holdScroll(at[0],at[1]);
|
|
160
226
|
// Again once the page has bound: a scroller's content is often drawn
|
|
161
227
|
// by a :for that has not run yet, and a box with nothing in it cannot
|
|
162
228
|
// be scrolled anywhere.
|
|
@@ -185,7 +251,7 @@ function routerSource() {
|
|
|
185
251
|
if(known){
|
|
186
252
|
var saved=readScroll(scrollToken);
|
|
187
253
|
if(saved&&(saved[0]||saved[1])){
|
|
188
|
-
var restoreAt=function(){window.scrollTo({left:saved[0],top:saved[1],behavior:'instant'})};
|
|
254
|
+
var restoreAt=function(){window.scrollTo({left:saved[0],top:saved[1],behavior:'instant'});holdScroll(saved[0],saved[1])};
|
|
189
255
|
if(document.readyState==='complete')restoreAt();
|
|
190
256
|
else window.addEventListener('load',restoreAt,{once:true});
|
|
191
257
|
}
|
|
@@ -388,8 +454,9 @@ function routerSource() {
|
|
|
388
454
|
var progDelay=null;
|
|
389
455
|
function startProgress(){
|
|
390
456
|
if(!o.progress||!progEl)return;
|
|
391
|
-
|
|
392
|
-
|
|
457
|
+
// Already counting for this tap (it waited on a prefetch, then went on
|
|
458
|
+
// to swap): keep the bar where it is rather than start it again.
|
|
459
|
+
if(progTimer||progDelay)return;
|
|
393
460
|
progDelay=setTimeout(function(){
|
|
394
461
|
progDelay=null;
|
|
395
462
|
setProgress(8);
|
|
@@ -1092,6 +1159,8 @@ function routerSource() {
|
|
|
1092
1159
|
return fetch(url,init);
|
|
1093
1160
|
}
|
|
1094
1161
|
function navigate(url,pushState,force){
|
|
1162
|
+
releaseScrollHold();
|
|
1163
|
+
winSnap=undefined;
|
|
1095
1164
|
var ctx=navCtxNext||{};
|
|
1096
1165
|
navCtxNext=null;
|
|
1097
1166
|
if(pushState&&typeof pushState==='object'){if(pushState.instant)instantNext=true;pushState=pushState.replace?'replace':true}
|
|
@@ -1128,6 +1197,14 @@ function routerSource() {
|
|
|
1128
1197
|
var awaitedKey=cacheKey(url);
|
|
1129
1198
|
if(o.cache&&!force&&!cache[awaitedKey]&&prefetching[awaitedKey]&&prefetching[awaitedKey].then){
|
|
1130
1199
|
awaitingKey=awaitedKey;
|
|
1200
|
+
// Answer the tap now, not when the prefetch lands. The page it asked
|
|
1201
|
+
// for is on the way, but until then nothing on screen said so: no
|
|
1202
|
+
// loading class, no progress bar. On a server a second or two away
|
|
1203
|
+
// (the hover started the fetch a moment before the click) a link read
|
|
1204
|
+
// as dead, and was clicked again, and again (hq.training's Calendar).
|
|
1205
|
+
// The navigation this hands over to keeps both going and ends them.
|
|
1206
|
+
document.body.classList.add(o.loadingClass);
|
|
1207
|
+
startProgress();
|
|
1131
1208
|
return prefetching[awaitedKey].then(function(){
|
|
1132
1209
|
if(awaitingKey!==awaitedKey)return false;
|
|
1133
1210
|
awaitingKey='';
|
|
@@ -1187,9 +1264,15 @@ function routerSource() {
|
|
|
1187
1264
|
return swapWith(html,targetPath);
|
|
1188
1265
|
}).catch(failed).finally(done);
|
|
1189
1266
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1267
|
+
var cached=cache[targetPath];
|
|
1268
|
+
var swapCached=function(){
|
|
1269
|
+
if(stale())return false;
|
|
1270
|
+
pendingContainerAttrs=attrsCache[targetPath]||'';
|
|
1271
|
+
pendingLayoutDecl=null;
|
|
1272
|
+
return swapWith(cached,targetPath);
|
|
1273
|
+
};
|
|
1274
|
+
var loading=beforeSwap(cached,url);
|
|
1275
|
+
return (loading?loading.then(function(ok){if(!ok){if(!stale())recoverUnbound(url,ctx);return false}return swapCached()}):Promise.resolve(swapCached())).then(function(swapped){
|
|
1193
1276
|
if(swapped===false)return false;
|
|
1194
1277
|
applyTitle(titleCache[targetPath]);
|
|
1195
1278
|
// Shown from the cache at once; asked again behind it, so a page that
|
|
@@ -1253,6 +1336,14 @@ else {
|
|
|
1253
1336
|
if(!result||stale())return false;
|
|
1254
1337
|
if(result.isFragment)result.html=fragmentMarker(result.runtime)+result.html;
|
|
1255
1338
|
if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
1339
|
+
var loading=beforeSwap(result.html,url);
|
|
1340
|
+
return loading?loading.then(function(ok){
|
|
1341
|
+
if(ok||stale())return result;
|
|
1342
|
+
recoverUnbound(url,ctx);
|
|
1343
|
+
return null;
|
|
1344
|
+
}):result;
|
|
1345
|
+
}).then(function(result){
|
|
1346
|
+
if(!result||stale())return false;
|
|
1256
1347
|
pendingContainerAttrs=result.isFragment?(result.containerAttrs||''):'';
|
|
1257
1348
|
pendingLayoutDecl=result.isFragment?null:{layout:result.layout||'',group:result.layoutGroup||''};
|
|
1258
1349
|
return Promise.resolve(swapWith(result.html,targetPath)).then(function(swapped){
|
|
@@ -1785,7 +1876,7 @@ else {
|
|
|
1785
1876
|
var useScreens=screensOn();
|
|
1786
1877
|
function startFragSwap(){
|
|
1787
1878
|
if(ctx.id&&ctx.id!==navSeq)return Promise.resolve(false);
|
|
1788
|
-
if(pushState!==false)
|
|
1879
|
+
if(pushState!==false)snapScroll();
|
|
1789
1880
|
if(useScreens)settleAnimation();
|
|
1790
1881
|
else if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
|
|
1791
1882
|
return new Promise(function(resolve,reject){
|
|
@@ -2254,7 +2345,7 @@ else {
|
|
|
2254
2345
|
|
|
2255
2346
|
function startSwap(){
|
|
2256
2347
|
if(ctx.id&&ctx.id!==navSeq)return Promise.resolve(false);
|
|
2257
|
-
if(pushState!==false)
|
|
2348
|
+
if(pushState!==false)snapScroll();
|
|
2258
2349
|
setDirection(ctx.direction);
|
|
2259
2350
|
// A whole document is written into the container as it is, so retained
|
|
2260
2351
|
// screens go first: they would otherwise outlive the page they belong to.
|
|
@@ -3027,7 +3118,7 @@ else {
|
|
|
3027
3118
|
sightObserver.unobserve(en.target);
|
|
3028
3119
|
if(sightCount>=o.prefetchVisibleMax)return;
|
|
3029
3120
|
sightCount++;
|
|
3030
|
-
prefetchLink(en.target);
|
|
3121
|
+
prefetchLink(en.target,false,true);
|
|
3031
3122
|
});
|
|
3032
3123
|
});
|
|
3033
3124
|
sightCount=0;
|
|
@@ -3340,14 +3431,24 @@ else {
|
|
|
3340
3431
|
// prefetch has finished waits for that answer instead of asking again.
|
|
3341
3432
|
// 'fresh' refetches a page that is cached, for an entry restored from an
|
|
3342
3433
|
// earlier launch that is shown at once and replaced behind it.
|
|
3343
|
-
|
|
3434
|
+
// 'glance' is a link that merely came into view (prefetchVisible): its
|
|
3435
|
+
// page is fetched, but its module bundles are left for an intent (a hover,
|
|
3436
|
+
// a touch) or the tap itself, which waits for them (beforeSwap). Each page
|
|
3437
|
+
// links bundles of its own, hq.training's ~4.7MB of script apiece, and a
|
|
3438
|
+
// screen with a dozen links in view used to download and evaluate every
|
|
3439
|
+
// one of them on arrival, competing with the page being read.
|
|
3440
|
+
function prefetchLink(link,fresh,glance){
|
|
3344
3441
|
// Same opt-out set as the click path. Without it, hovering a link the
|
|
3345
3442
|
// router is not allowed to claim still fired a real GET at it \u2014 a
|
|
3346
3443
|
// logout or OAuth URL was requested on hover alone.
|
|
3347
3444
|
if(isRouterExcluded(link))return;
|
|
3348
3445
|
var href=withCurrentLocale(link.getAttribute('href'));
|
|
3349
3446
|
var key=cacheKey(href);
|
|
3350
|
-
if(
|
|
3447
|
+
if(cache[key]&&!fresh){if(!glance)loadModuleRegistries(cache[key],href);return}
|
|
3448
|
+
if(prefetching[key]){
|
|
3449
|
+
if(!glance)prefetching[key].then(function(){if(cache[key])loadModuleRegistries(cache[key],href)});
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3351
3452
|
var eager=link.getAttribute('data-stx-prefetch')==='eager';
|
|
3352
3453
|
var wantsFragment=shouldUseFragmentResponse();
|
|
3353
3454
|
prefetching[key]=fetch(href,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
|
|
@@ -3358,7 +3459,7 @@ else {
|
|
|
3358
3459
|
if(eager)keepPage(key,result);
|
|
3359
3460
|
}
|
|
3360
3461
|
if(result){
|
|
3361
|
-
loadModuleRegistries(result.html,href);
|
|
3462
|
+
if(!glance)loadModuleRegistries(result.html,href);
|
|
3362
3463
|
// Its stylesheet too, so the tap never waits on cssLoadTimeout for a
|
|
3363
3464
|
// sheet it could have had all along. Added unapplied (media=print),
|
|
3364
3465
|
// as a navigation adds it; the swap makes it live.
|
|
@@ -3411,14 +3512,41 @@ else {
|
|
|
3411
3512
|
// registers modules, once, so loading it early changes nothing but timing,
|
|
3412
3513
|
// and the tap then finds every script it needs already here: the swap runs
|
|
3413
3514
|
// in one task and the first visit to a tab paints whole, like the second.
|
|
3515
|
+
//
|
|
3516
|
+
// The composables bundle too (also stx's own /_stx/modules.<hash>.js): it
|
|
3517
|
+
// only registers what is not registered yet. Answers a promise for the
|
|
3518
|
+
// files still on their way, or null when there are none.
|
|
3414
3519
|
function loadModuleRegistries(html,base){
|
|
3520
|
+
var waits=[];
|
|
3415
3521
|
html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>','gi'),function(m,attrs){
|
|
3416
|
-
if(!/(?:^|\\s)data-stx-modules(?:[\\s=]|$)/i.test(attrs))return m;
|
|
3417
3522
|
var srcMatch=attrs.match(/\\bsrc\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
3418
3523
|
var src=srcMatch&&(srcMatch[1]||srcMatch[2]||srcMatch[3]);
|
|
3419
|
-
if(src)
|
|
3524
|
+
if(!src)return m;
|
|
3525
|
+
if(!/(?:^|\\s)data-stx-modules(?:[\\s=]|$)/i.test(attrs)&&!isModuleBundle(src))return m;
|
|
3526
|
+
var pending=loadExternalScript(src,base);
|
|
3527
|
+
if(pending)waits.push(pending);
|
|
3420
3528
|
return m;
|
|
3421
3529
|
});
|
|
3530
|
+
return waits.length?Promise.all(waits):null;
|
|
3531
|
+
}
|
|
3532
|
+
// A page swapped in before its module bundles have loaded is drawn bare
|
|
3533
|
+
// (everything behind :if and x-cloak) and bound a network round trip
|
|
3534
|
+
// later: the main area went blank for a moment on every page the router
|
|
3535
|
+
// had not prefetched -- Back to the page the app opened on, a link it never
|
|
3536
|
+
// hovered. Waiting for them first keeps the outgoing page up, under the
|
|
3537
|
+
// progress bar, and lets the swap, its scripts and the first paint happen
|
|
3538
|
+
// in one task. Capped, so a bundle that hangs costs a delay, not the page.
|
|
3539
|
+
// Null when there is nothing to wait for, so a cached page still swaps in
|
|
3540
|
+
// the task of the tap that asked for it.
|
|
3541
|
+
// Answers false when a bundle failed: that page cannot bind, and is
|
|
3542
|
+
// recovered (recoverUnbound) rather than swapped in bare.
|
|
3543
|
+
function beforeSwap(html,base){
|
|
3544
|
+
var waits=loadModuleRegistries(html,base);
|
|
3545
|
+
if(!waits)return null;
|
|
3546
|
+
return Promise.race([
|
|
3547
|
+
waits.then(function(results){return results.indexOf(false)===-1}),
|
|
3548
|
+
new Promise(function(resolve){setTimeout(function(){resolve(true)},4000)}),
|
|
3549
|
+
]);
|
|
3422
3550
|
}
|
|
3423
3551
|
|
|
3424
3552
|
// Links marked data-stx-prefetch="eager", fetched once the page is idle, so
|
|
@@ -3449,6 +3577,25 @@ else {
|
|
|
3449
3577
|
else setTimeout(run,100);
|
|
3450
3578
|
}
|
|
3451
3579
|
|
|
3580
|
+
// The page the app was opened on came as a whole document, so it is the
|
|
3581
|
+
// one page the cache never holds: Back to it went to the server and left
|
|
3582
|
+
// the page being read on screen under the new address until it answered
|
|
3583
|
+
// (a second or more on a distant server). Once the page is idle its
|
|
3584
|
+
// fragment is fetched, the same request a link to it would make, so Back
|
|
3585
|
+
// is a swap like every other.
|
|
3586
|
+
function keepOpeningPage(){
|
|
3587
|
+
if(!o.cache||!shouldUseFragmentResponse())return;
|
|
3588
|
+
var c=navigator.connection;
|
|
3589
|
+
if(c&&(c.saveData||/2g/.test(c.effectiveType||'')))return;
|
|
3590
|
+
var url=location.pathname+location.search;
|
|
3591
|
+
var key=cacheKey(url);
|
|
3592
|
+
// Late by design, so it can find the page gone (a test's document torn
|
|
3593
|
+
// down, a full navigation begun): never let that throw from a timer.
|
|
3594
|
+
var run=function(){try{if(!cache[key]&&!prefetching[key])revalidate(key,url)}catch(e){}};
|
|
3595
|
+
if(window.requestIdleCallback)window.requestIdleCallback(run,{timeout:3000});
|
|
3596
|
+
else setTimeout(run,1500);
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3452
3599
|
if(o.prefetch){
|
|
3453
3600
|
// A mouse hovers before it clicks. A finger does not hover, but it
|
|
3454
3601
|
// touches down a tap's length before the click: start the fetch there.
|
|
@@ -3736,6 +3883,8 @@ else {
|
|
|
3736
3883
|
restoreKeptPages();
|
|
3737
3884
|
prefetchEager();
|
|
3738
3885
|
observeLinks();
|
|
3886
|
+
// The runtime's first stx:load has usually gone by when this runs.
|
|
3887
|
+
if(o.prefetch&&o.keepOpening!==false)keepOpeningPage();
|
|
3739
3888
|
}
|
|
3740
3889
|
|
|
3741
3890
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stx-router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.529",
|
|
5
5
|
"description": "File-based router for STX — .stx template discovery, nested layouts, typed route params, middleware, and client-side SPA navigation.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|