stx-router 0.2.384 → 0.2.386
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 +108 -17
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -203,6 +203,83 @@ function routerSource() {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// \u2500\u2500 Instant navigation: one frame, already filled \u2500\u2500
|
|
207
|
+
// A tab swap used to paint the new screen before its scripts had run: a page
|
|
208
|
+
// script with an import is a module, and a module runs after the task that
|
|
209
|
+
// inserted it, so for a few frames the screen showed its bare markup and then
|
|
210
|
+
// filled in, on every visit. Each inserted page script now reports when it
|
|
211
|
+
// has run, and an instant navigation holds the old frame (a View Transition
|
|
212
|
+
// with no animation) until they all have, capped so it can never hang.
|
|
213
|
+
var scriptsPending=0;
|
|
214
|
+
var hydrateWaiters=[];
|
|
215
|
+
window.__stxScriptRan=function(){
|
|
216
|
+
if(scriptsPending>0)scriptsPending--;
|
|
217
|
+
if(!scriptsPending){var w=hydrateWaiters;hydrateWaiters=[];w.forEach(function(f){f()})}
|
|
218
|
+
};
|
|
219
|
+
function markRan(code){scriptsPending++;return code+String.fromCharCode(10)+';window.__stxScriptRan&&window.__stxScriptRan();'}
|
|
220
|
+
function whenHydrated(capMs){
|
|
221
|
+
return new Promise(function(resolve){
|
|
222
|
+
var done=false;
|
|
223
|
+
function finish(){if(done)return;done=true;setTimeout(resolve,0)}
|
|
224
|
+
if(!scriptsPending){done=true;resolve();return}
|
|
225
|
+
hydrateWaiters.push(finish);
|
|
226
|
+
setTimeout(finish,capMs);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Whether a fragment's scripts all run inside the swap's own task: none is
|
|
230
|
+
// a module, and every file it names is already loaded. Then an instant
|
|
231
|
+
// navigation needs no held frame at all: swapped, run and bound before the
|
|
232
|
+
// browser next paints, it shows the new screen whole in the first frame.
|
|
233
|
+
function swapRunsInOneTask(html,base){
|
|
234
|
+
var sync=true;
|
|
235
|
+
html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/scr'+'ipt>','gi'),function(m,attrs,code){
|
|
236
|
+
if(!sync)return m;
|
|
237
|
+
var typeMatch=attrs.match(/\\btype\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
238
|
+
var scriptType=((typeMatch&&(typeMatch[1]||typeMatch[2]||typeMatch[3]))||'').trim().toLowerCase();
|
|
239
|
+
if(scriptType==='module'){sync=false;return m}
|
|
240
|
+
if(scriptType&&scriptType!=='text/javascript'&&scriptType!=='application/javascript')return m;
|
|
241
|
+
var srcMatch=attrs.match(/\\bsrc\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
242
|
+
var src=srcMatch&&(srcMatch[1]||srcMatch[2]||srcMatch[3]);
|
|
243
|
+
if(src){
|
|
244
|
+
if(!isSharedClientScriptAttrs(attrs)){var key=externalScriptKey(src,base);if(key&&!loadedExternalScripts[key])sync=false}
|
|
245
|
+
return m;
|
|
246
|
+
}
|
|
247
|
+
if(code&&hasStaticImport(code))sync=false;
|
|
248
|
+
return m;
|
|
249
|
+
});
|
|
250
|
+
return sync;
|
|
251
|
+
}
|
|
252
|
+
function runInOneTask(complete){
|
|
253
|
+
// The runtime binds on stx:load after a debounce; bind now, in this task.
|
|
254
|
+
function flush(){if(window.stx&&typeof window.stx._flushLoad==='function')window.stx._flushLoad()}
|
|
255
|
+
window.addEventListener('stx:load',flush,{once:true});
|
|
256
|
+
try{complete()}finally{window.removeEventListener('stx:load',flush)}
|
|
257
|
+
}
|
|
258
|
+
function runInstantSwap(complete){
|
|
259
|
+
if(!document.startViewTransition)return false;
|
|
260
|
+
var root=document.documentElement;
|
|
261
|
+
root.classList.add('stx-instant');
|
|
262
|
+
var clear=function(){root.classList.remove('stx-instant')};
|
|
263
|
+
try{
|
|
264
|
+
var transition=document.startViewTransition(function(){
|
|
265
|
+
scriptsPending=0;
|
|
266
|
+
// The runtime binds the page on stx:load after a short debounce; run
|
|
267
|
+
// it now, so the frame this reveals is bound rather than showing its
|
|
268
|
+
// {{ }} for a frame before they fill in.
|
|
269
|
+
var loaded=new Promise(function(resolve){window.addEventListener('stx:load',function(){if(window.stx&&typeof window.stx._flushLoad==='function')window.stx._flushLoad();resolve()},{once:true});setTimeout(resolve,300)});
|
|
270
|
+
complete();
|
|
271
|
+
return loaded.then(function(){return whenHydrated(300)});
|
|
272
|
+
});
|
|
273
|
+
if(transition&&transition.finished&&transition.finished.then)transition.finished.then(clear,clear);else clear();
|
|
274
|
+
if(transition&&transition.ready&&transition.ready.catch)transition.ready.catch(function(){});
|
|
275
|
+
return true;
|
|
276
|
+
}catch(err){
|
|
277
|
+
clear();
|
|
278
|
+
log('[router] instant transition unavailable:',err&&err.message?err.message:err);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
206
283
|
// \u2500\u2500 Progress bar (0\u2192100% at top of viewport) \u2500\u2500
|
|
207
284
|
// Native loading indicator baked into the router. Starts when navigation
|
|
208
285
|
// begins, trickles up to ~90% during fetch, snaps to 100% + fades on
|
|
@@ -514,19 +591,13 @@ function routerSource() {
|
|
|
514
591
|
* Column-anchored: a declaration nested inside a function is indented, and
|
|
515
592
|
* is not in scope at the end of the block anyway.
|
|
516
593
|
*/
|
|
517
|
-
var
|
|
594
|
+
var TLD=/^(?:export\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|const|let|var)\\s+([$\\w]+)/gm;
|
|
518
595
|
function republishTopLevel(src){
|
|
519
|
-
var
|
|
520
|
-
|
|
521
|
-
while((m=
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if(!names.length)return '';
|
|
525
|
-
var out='\\n;';
|
|
526
|
-
for(var i=0;i<names.length;i++){
|
|
527
|
-
out+='try{if(typeof '+names[i]+'==="function")window.'+names[i]+'='+names[i]+'}catch(e){}';
|
|
528
|
-
}
|
|
529
|
-
return out;
|
|
596
|
+
var n=[],m,o='',i;
|
|
597
|
+
TLD.lastIndex=0;
|
|
598
|
+
while((m=TLD.exec(src))!==null)if(n.indexOf(m[1])<0)n.push(m[1]);
|
|
599
|
+
for(i=0;i<n.length;i++)o+='try{typeof '+n[i]+'=="function"&&(window.'+n[i]+'='+n[i]+')}catch(e){}';
|
|
600
|
+
return o?'\\n;'+o:'';
|
|
530
601
|
}
|
|
531
602
|
function runsAlways(declared, code){
|
|
532
603
|
if(declared==='always')return true;
|
|
@@ -1334,7 +1405,7 @@ else {
|
|
|
1334
1405
|
if(hasImport){
|
|
1335
1406
|
ns.type='module';
|
|
1336
1407
|
}
|
|
1337
|
-
ns.textContent=(hasImport||isAlreadyScoped)?code:'{'+code+'}';
|
|
1408
|
+
ns.textContent=markRan((hasImport||isAlreadyScoped)?code:'{'+code+'}');
|
|
1338
1409
|
ns.setAttribute('data-stx-page','');
|
|
1339
1410
|
if(placeholder&&placeholder.parentNode){
|
|
1340
1411
|
ns.setAttribute('data-stx-positioned','');
|
|
@@ -1375,7 +1446,9 @@ else {
|
|
|
1375
1446
|
resolve(true);
|
|
1376
1447
|
}catch(err){reject(err)}
|
|
1377
1448
|
}
|
|
1378
|
-
if(
|
|
1449
|
+
if(instantNav&&swapRunsInOneTask(html,url)){runInOneTask(completeFragSwap)}
|
|
1450
|
+
else if(instantNav&&runInstantSwap(completeFragSwap)){}
|
|
1451
|
+
else if(runViewTransition(completeFragSwap)){}
|
|
1379
1452
|
else if(instantNav){completeFragSwap()}
|
|
1380
1453
|
else{currentContent.style.transition='opacity 0.12s ease-out';currentContent.style.opacity='0';setTimeout(function(){completeFragSwap();currentContent.style.opacity='1';setTimeout(function(){currentContent.style.transition=''},150)},120)}
|
|
1381
1454
|
});
|
|
@@ -1791,7 +1864,7 @@ else {
|
|
|
1791
1864
|
// ESM, and top-level 'import' is illegal inside a block, which
|
|
1792
1865
|
// would throw SyntaxError before the script ever runs.
|
|
1793
1866
|
var alreadyScoped=runsAlways(typeof entry==='string'?'':entry.run,text);
|
|
1794
|
-
ns.textContent=(hasImport||alreadyScoped)?text:'{'+text+republishTopLevel(text)+'}';
|
|
1867
|
+
ns.textContent=markRan((hasImport||alreadyScoped)?text:'{'+text+republishTopLevel(text)+'}');
|
|
1795
1868
|
ns.setAttribute('data-stx-page','');
|
|
1796
1869
|
var placeholder=entry.slot?qs('script[data-stx-route-script="'+entry.slot+'"]'):null;
|
|
1797
1870
|
if(placeholder&&placeholder.parentNode){
|
|
@@ -1825,7 +1898,9 @@ else {
|
|
|
1825
1898
|
resolve(true);
|
|
1826
1899
|
}catch(err){reject(err)}
|
|
1827
1900
|
}
|
|
1828
|
-
if(
|
|
1901
|
+
if(instantNav&&runInstantSwap(completeSwap)){
|
|
1902
|
+
}
|
|
1903
|
+
else if(runViewTransition(completeSwap)){
|
|
1829
1904
|
}
|
|
1830
1905
|
else if(instantNav){
|
|
1831
1906
|
// A tab: swap at once, no fade either way.
|
|
@@ -2120,9 +2195,25 @@ else {
|
|
|
2120
2195
|
return readPrefetchResponse(r,wantsFragment);
|
|
2121
2196
|
}).then(function(result){
|
|
2122
2197
|
if(result&&o.cache)setCache(key,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
|
|
2198
|
+
if(result)loadModuleRegistries(result.html,href);
|
|
2123
2199
|
}).catch(function(){}).finally(function(){delete prefetching[key]});
|
|
2124
2200
|
}
|
|
2125
2201
|
|
|
2202
|
+
// A prefetched page's module registry (the file its imports are bundled
|
|
2203
|
+
// into, one per page) is loaded with the prefetch, not on the tap. It only
|
|
2204
|
+
// registers modules, once, so loading it early changes nothing but timing,
|
|
2205
|
+
// and the tap then finds every script it needs already here: the swap runs
|
|
2206
|
+
// in one task and the first visit to a tab paints whole, like the second.
|
|
2207
|
+
function loadModuleRegistries(html,base){
|
|
2208
|
+
html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>','gi'),function(m,attrs){
|
|
2209
|
+
if(!/(?:^|\\s)data-stx-modules(?:[\\s=]|$)/i.test(attrs))return m;
|
|
2210
|
+
var srcMatch=attrs.match(/\\bsrc\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
2211
|
+
var src=srcMatch&&(srcMatch[1]||srcMatch[2]||srcMatch[3]);
|
|
2212
|
+
if(src)loadExternalScript(src,base);
|
|
2213
|
+
return m;
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2126
2217
|
// Links marked data-stx-prefetch="eager", fetched once the page is idle, so
|
|
2127
2218
|
// even their first tap is served from the cache: the way a phone's tab bar
|
|
2128
2219
|
// switches instantly. Not on a connection that asked to save data.
|
|
@@ -2317,7 +2408,7 @@ else {
|
|
|
2317
2408
|
// The container the router focuses after a navigation is not a
|
|
2318
2409
|
// control, so it draws no focus ring: Safari drew one around the whole
|
|
2319
2410
|
// page, a blue line along its bottom edge on every screen.
|
|
2320
|
-
var css='[data-stx-route-focus]:focus{outline:none}.stx-navigating{cursor:wait}.stx-navigating a,.stx-navigating button{pointer-events:none}#stx-router-progress{position:fixed;top:0;left:0;right:0;height:'+ph+';background:'+pc+';box-shadow:0 0 8px '+pc+',0 0 4px '+pc+';transform:scaleX(0);transform-origin:left;transition:transform .18s ease-out,opacity .26s ease;opacity:0;pointer-events:none;z-index:999999}';
|
|
2411
|
+
var css='[data-stx-route-focus]:focus{outline:none}.stx-navigating{cursor:wait}.stx-navigating a,.stx-navigating button{pointer-events:none}#stx-router-progress{position:fixed;top:0;left:0;right:0;height:'+ph+';background:'+pc+';box-shadow:0 0 8px '+pc+',0 0 4px '+pc+';transform:scaleX(0);transform-origin:left;transition:transform .18s ease-out,opacity .26s ease;opacity:0;pointer-events:none;z-index:999999}html.stx-instant::view-transition-group(*),html.stx-instant::view-transition-old(*),html.stx-instant::view-transition-new(*){animation:none!important}';
|
|
2321
2412
|
if(o.viewTransitions&&'startViewTransition' in document){
|
|
2322
2413
|
var dur=(o.viewTransitionDuration||220)+'ms';
|
|
2323
2414
|
var ease=o.viewTransitionEasing||'cubic-bezier(0.16, 1, 0.3, 1)';
|
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.386",
|
|
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",
|