stx-router 0.2.444 → 0.2.449
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 +85 -25
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -850,15 +850,28 @@ function routerSource() {
|
|
|
850
850
|
if(key)loadedExternalScripts[key]=1;
|
|
851
851
|
return key;
|
|
852
852
|
}
|
|
853
|
-
// Returns a promise, or null when there is
|
|
854
|
-
// loaded file, or an unparseable src.
|
|
855
|
-
// one 404 must not stop the rest of
|
|
856
|
-
//
|
|
853
|
+
// Returns a promise of whether the file loaded, or null when there is
|
|
854
|
+
// nothing to wait for \u2014 an already loaded file, or an unparseable src.
|
|
855
|
+
// Resolves rather than rejects on error: one 404 must not stop the rest of
|
|
856
|
+
// the page hydrating; the caller decides what a failure means.
|
|
857
|
+
//
|
|
858
|
+
// A file still on its way is not a loaded one. A prefetch starts loading
|
|
859
|
+
// the page's module registry (loadModuleRegistries), and this used to count
|
|
860
|
+
// it loaded the moment it was requested, so a tap that beat it swapped the
|
|
861
|
+
// page in and ran its setup ahead of the registry it imports from: "is not
|
|
862
|
+
// registered on this page", every binding dead. Locally a file loads in a
|
|
863
|
+
// millisecond and the tap never wins; on a phone far from the server it
|
|
864
|
+
// does. The tap now waits on the same request.
|
|
865
|
+
//
|
|
866
|
+
// And a file that failed is forgotten, so a fresh copy of the page that
|
|
867
|
+
// links it again (recoverUnbound) asks for it again rather than finding it
|
|
868
|
+
// "loaded" and running without it until the next reload.
|
|
869
|
+
var pendingExternalScripts={};
|
|
857
870
|
function loadExternalScript(src,base){
|
|
858
871
|
var key=externalScriptKey(src,base);
|
|
859
872
|
if(!key||loadedExternalScripts[key])return null;
|
|
860
|
-
|
|
861
|
-
return new Promise(function(resolve){
|
|
873
|
+
if(pendingExternalScripts[key])return pendingExternalScripts[key];
|
|
874
|
+
return pendingExternalScripts[key]=new Promise(function(resolve){
|
|
862
875
|
var el=ce('script');
|
|
863
876
|
el.src=key;
|
|
864
877
|
// Deliberately NOT data-stx-page: both swap paths clear those on every
|
|
@@ -869,11 +882,31 @@ function routerSource() {
|
|
|
869
882
|
// In document order: a fragment's composables bundle reads the module
|
|
870
883
|
// registry loaded beside it, and an inserted script is async by default.
|
|
871
884
|
el.async=false;
|
|
872
|
-
el.onload=function(){resolve()};
|
|
873
|
-
el.onerror=function(){
|
|
885
|
+
el.onload=function(){delete pendingExternalScripts[key];loadedExternalScripts[key]=1;resolve(true)};
|
|
886
|
+
el.onerror=function(){
|
|
887
|
+
log('[router] container script failed:',key);
|
|
888
|
+
delete pendingExternalScripts[key];
|
|
889
|
+
if(el.parentNode)el.parentNode.removeChild(el);
|
|
890
|
+
resolve(false);
|
|
891
|
+
};
|
|
874
892
|
dhead().appendChild(el);
|
|
875
893
|
});
|
|
876
894
|
}
|
|
895
|
+
// A page's module registry, stores or composables in serve mode: stx's own
|
|
896
|
+
// file, named by its content (registerServeModuleBundle). Without it the
|
|
897
|
+
// page's setup cannot run at all, unlike an app's own script, whose
|
|
898
|
+
// failure the rest of the page survives.
|
|
899
|
+
var MODULE_BUNDLE=/\\/_stx\\/modules\\.[0-9a-f]{16}\\.js(?:[?#]|$)/;
|
|
900
|
+
function isModuleBundle(src){return MODULE_BUNDLE.test(src||'')}
|
|
901
|
+
// Waits for the files a swap loads, then runs its scripts -- or, when one
|
|
902
|
+
// of them was a module bundle that did not load, recovers the page instead
|
|
903
|
+
// of running a setup that can only throw.
|
|
904
|
+
function afterExternalScripts(pending,run,url,ctx){
|
|
905
|
+
if(!pending.length){run();return}
|
|
906
|
+
var failed=false;
|
|
907
|
+
Promise.all(pending.map(function(p){return p.promise.then(function(ok){if(ok===false&&isModuleBundle(p.src))failed=true})}))
|
|
908
|
+
.then(function(){if(failed)recoverUnbound(url,ctx);else run()},run);
|
|
909
|
+
}
|
|
877
910
|
|
|
878
911
|
// Record scripts from the initial page load
|
|
879
912
|
qsa('script').forEach(function(s){
|
|
@@ -997,9 +1030,13 @@ function routerSource() {
|
|
|
997
1030
|
location.href=away;
|
|
998
1031
|
return true;
|
|
999
1032
|
}
|
|
1000
|
-
|
|
1033
|
+
// fresh: past every cache, the offline worker's included (it hands a
|
|
1034
|
+
// no-store request straight to the network). For recoverUnbound, whose
|
|
1035
|
+
// whole point is that the kept copy is the broken one.
|
|
1036
|
+
function routerFetch(url,headers,fresh){
|
|
1001
1037
|
var init={headers:headers};
|
|
1002
1038
|
if(navAbort)init.signal=navAbort.signal;
|
|
1039
|
+
if(fresh)init.cache='no-store';
|
|
1003
1040
|
return fetch(url,init);
|
|
1004
1041
|
}
|
|
1005
1042
|
function navigate(url,pushState,force){
|
|
@@ -1115,7 +1152,7 @@ else {
|
|
|
1115
1152
|
// the page content. Custom app-shell containers need full documents so
|
|
1116
1153
|
// the router does not inject a <main> fragment into the wrong element.
|
|
1117
1154
|
var wantsFragment=shouldUseFragmentResponse();
|
|
1118
|
-
return routerFetch(url,wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}).then(function(r){
|
|
1155
|
+
return routerFetch(url,wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'},ctx.fresh).then(function(r){
|
|
1119
1156
|
if(stale())return null;
|
|
1120
1157
|
if(leaveOrigin(r))return null;
|
|
1121
1158
|
if(!r.ok)throw httpError(r.status);
|
|
@@ -1149,7 +1186,7 @@ else {
|
|
|
1149
1186
|
// Layout change? Fetch the FULL page (no X-STX-Router header) and do full document swap
|
|
1150
1187
|
if(isFragment&&checkLayoutChange(newLayout,url,newGroup)){
|
|
1151
1188
|
log('[router] layout change \u2014 fetching full page for document swap');
|
|
1152
|
-
return routerFetch(url,{'Accept':'text/html'}).then(function(fullRes){
|
|
1189
|
+
return routerFetch(url,{'Accept':'text/html'},ctx.fresh).then(function(fullRes){
|
|
1153
1190
|
log('[router] full page fetched:',fullRes.status,'ok:',fullRes.ok);
|
|
1154
1191
|
if(leaveOrigin(fullRes))return null;
|
|
1155
1192
|
if(!fullRes.ok)throw httpError(fullRes.status);
|
|
@@ -1198,6 +1235,33 @@ else {
|
|
|
1198
1235
|
pendingCtx=ctx;
|
|
1199
1236
|
return Promise.resolve(swap(html,cacheKey(url),pushState,hash)).then(function(){return false});
|
|
1200
1237
|
}
|
|
1238
|
+
// \u2500\u2500 A page whose module bundle did not load \u2500\u2500
|
|
1239
|
+
// Serve mode links a page's registry, stores and composables as
|
|
1240
|
+
// /_stx/modules.<hash>.js, answered from what the server process has
|
|
1241
|
+
// rendered. Its HTML can outlive that process: the router's cache, a
|
|
1242
|
+
// prefetch, the offline worker's kept copy. After a deploy (a new release
|
|
1243
|
+
// directory, a new process that has never rendered the page) the hash is
|
|
1244
|
+
// answered 404, and the page was swapped in with nothing bound -- its
|
|
1245
|
+
// {{ }} empty, its :for lists empty, its :if buttons gone -- until a
|
|
1246
|
+
// revalidation happened to replace the copy (hq.training, a coach's
|
|
1247
|
+
// /coaches/athletes).
|
|
1248
|
+
//
|
|
1249
|
+
// So that copy is dropped and the page fetched again past every cache:
|
|
1250
|
+
// the process answering renders it, which registers the bundle it links,
|
|
1251
|
+
// and a newer build is caught by the skew check and loaded whole (#1772).
|
|
1252
|
+
// Once only. A fresh copy whose bundle fails too is loaded as a document,
|
|
1253
|
+
// where the browser and the server settle it without the router.
|
|
1254
|
+
//
|
|
1255
|
+
// Not a page whose own setup throws: that throws again on a fresh copy, so
|
|
1256
|
+
// retrying would only add a round trip and a reload to every visit.
|
|
1257
|
+
function recoverUnbound(url,ctx){
|
|
1258
|
+
if(ctx&&ctx.id&&ctx.id!==navSeq)return;
|
|
1259
|
+
evictCache(cacheKey(url));
|
|
1260
|
+
if(ctx&&ctx.fresh){log('[router] module bundle failed again \u2014 full navigation to:',url);location.href=url;return}
|
|
1261
|
+
log('[router] module bundle failed \u2014 fetching',url,'again past the caches');
|
|
1262
|
+
navCtxNext={direction:'replace',fresh:true};
|
|
1263
|
+
navigate(url,'replace',true);
|
|
1264
|
+
}
|
|
1201
1265
|
function retryShown(){var root=activeRoot();var el=root&&root.querySelector?root.querySelector('[data-stx-retry]'):null;return el?el.getAttribute('data-stx-retry'):''}
|
|
1202
1266
|
document.addEventListener('click',function(e){
|
|
1203
1267
|
var b=e.target&&e.target.closest?e.target.closest('[data-stx-retry-button]'):null;
|
|
@@ -1632,10 +1696,9 @@ else {
|
|
|
1632
1696
|
var fragPending=[];
|
|
1633
1697
|
fragExternalScripts.forEach(function(src){
|
|
1634
1698
|
var pending=loadExternalScript(src,url);
|
|
1635
|
-
if(pending)fragPending.push(pending);
|
|
1699
|
+
if(pending)fragPending.push({src:src,promise:pending});
|
|
1636
1700
|
});
|
|
1637
|
-
|
|
1638
|
-
else runFragScripts();
|
|
1701
|
+
afterExternalScripts(fragPending,runFragScripts,url+(hash||''),ctx);
|
|
1639
1702
|
}
|
|
1640
1703
|
// With screens retained, the outgoing page is kept (or disposed once it
|
|
1641
1704
|
// has animated away) by screenBegin rather than cleaned up here.
|
|
@@ -1900,13 +1963,15 @@ else {
|
|
|
1900
1963
|
var src=new URL(s.getAttribute('src'),docBase).href;
|
|
1901
1964
|
if(loadedSrcs[src])return;
|
|
1902
1965
|
loadedSrcs[src]=1;
|
|
1903
|
-
extPromises.push(new Promise(function(resolve
|
|
1966
|
+
extPromises.push({src:src,promise:new Promise(function(resolve){
|
|
1904
1967
|
var ns=ce('script');
|
|
1905
1968
|
ns.src=src;
|
|
1906
|
-
ns.onload=resolve;
|
|
1907
|
-
|
|
1969
|
+
ns.onload=function(){resolve(true)};
|
|
1970
|
+
// Settled either way, as before (a rejection ran the scripts at
|
|
1971
|
+
// once); afterExternalScripts tells a failed module bundle apart.
|
|
1972
|
+
ns.onerror=function(){if(ns.parentNode)ns.parentNode.removeChild(ns);resolve(false)};
|
|
1908
1973
|
dhead().appendChild(ns);
|
|
1909
|
-
}));
|
|
1974
|
+
})});
|
|
1910
1975
|
});
|
|
1911
1976
|
|
|
1912
1977
|
// Before the inline scripts, and through the same barrier: an inline
|
|
@@ -1914,7 +1979,7 @@ else {
|
|
|
1914
1979
|
// so running it first would fail exactly the way this bug did.
|
|
1915
1980
|
routedExternalScripts.forEach(function(src){
|
|
1916
1981
|
var pending=loadExternalScript(src,docBase);
|
|
1917
|
-
if(pending)extPromises.push(pending);
|
|
1982
|
+
if(pending)extPromises.push({src:src,promise:pending});
|
|
1918
1983
|
});
|
|
1919
1984
|
|
|
1920
1985
|
// \u2500\u2500 Script re-execution \u2500\u2500
|
|
@@ -2089,12 +2154,7 @@ else {
|
|
|
2089
2154
|
window.dispatchEvent(new Event('stx:load'));
|
|
2090
2155
|
}
|
|
2091
2156
|
|
|
2092
|
-
|
|
2093
|
-
Promise.all(extPromises).then(execScripts).catch(execScripts);
|
|
2094
|
-
}
|
|
2095
|
-
else {
|
|
2096
|
-
execScripts();
|
|
2097
|
-
}
|
|
2157
|
+
afterExternalScripts(extPromises,execScripts,url+(hash||''),ctx);
|
|
2098
2158
|
}
|
|
2099
2159
|
|
|
2100
2160
|
function startSwap(){
|
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.449",
|
|
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",
|