stx-router 0.2.250 → 0.2.252
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 +93 -2
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -380,10 +380,68 @@ export function getRouterScript() {
|
|
|
380
380
|
function fragmentMarker(declared){
|
|
381
381
|
return '<!--stx-fragment'+(declared==='true'?' rt=1':declared==='false'?' rt=0':'')+'-->';
|
|
382
382
|
}
|
|
383
|
+
// \u2500\u2500 External scripts inside the routed container \u2500\u2500
|
|
384
|
+
//
|
|
385
|
+
// A page whose x-data factory lives in its own file \u2014 a
|
|
386
|
+
// <script src="dashboard-xdata.js"> sitting inside [data-stx-content] \u2014 lost that
|
|
387
|
+
// file on every SPA navigation. Both swap paths dropped it: the fragment
|
|
388
|
+
// path's regex sees an empty body and returns '', and the full-document path
|
|
389
|
+
// removed it outright. The inline scripts beside it were re-executed with
|
|
390
|
+
// care; the external ones were deleted.
|
|
391
|
+
//
|
|
392
|
+
// The result was a screen that rendered on a cold load and came back empty
|
|
393
|
+
// after a click, reporting "dashboardXData is not defined" with every binding
|
|
394
|
+
// in the container left unevaluated \u2014 because nothing had defined it since
|
|
395
|
+
// the file was thrown away.
|
|
396
|
+
//
|
|
397
|
+
// Loaded once per URL, never re-run. What these files define is a global that
|
|
398
|
+
// outlives the navigation, so a second execution buys nothing and costs
|
|
399
|
+
// whatever side effects the file has: a mount helper firing again on every
|
|
400
|
+
// click through the same page is its own bug.
|
|
401
|
+
var loadedExternalScripts={};
|
|
402
|
+
// The base is resolved first because callers pass what they have: the
|
|
403
|
+
// fragment path knows only the path it navigated to ('/reports/disk'), and
|
|
404
|
+
// new URL(src, '/reports/disk') throws \u2014 a base must be absolute. Silently
|
|
405
|
+
// returning '' there meant the file was never requested, which is the bug
|
|
406
|
+
// this whole block exists to fix, reintroduced one level down.
|
|
407
|
+
function externalScriptKey(src,base){
|
|
408
|
+
try{
|
|
409
|
+
var absoluteBase=base?new URL(base,location.href).href:location.href;
|
|
410
|
+
return new URL(src,absoluteBase).href;
|
|
411
|
+
}catch(e){return ''}
|
|
412
|
+
}
|
|
413
|
+
function rememberExternalScript(src,base){
|
|
414
|
+
var key=externalScriptKey(src,base);
|
|
415
|
+
if(key)loadedExternalScripts[key]=1;
|
|
416
|
+
return key;
|
|
417
|
+
}
|
|
418
|
+
// Returns a promise, or null when there is nothing to wait for \u2014 an already
|
|
419
|
+
// loaded file, or an unparseable src. Resolves rather than rejects on error:
|
|
420
|
+
// one 404 must not stop the rest of the page hydrating, matching what the
|
|
421
|
+
// head-script path already does by sending failures to execScripts too.
|
|
422
|
+
function loadExternalScript(src,base){
|
|
423
|
+
var key=externalScriptKey(src,base);
|
|
424
|
+
if(!key||loadedExternalScripts[key])return null;
|
|
425
|
+
loadedExternalScripts[key]=1;
|
|
426
|
+
return new Promise(function(resolve){
|
|
427
|
+
var el=document.createElement('script');
|
|
428
|
+
el.src=key;
|
|
429
|
+
// Deliberately NOT data-stx-page: both swap paths clear those on every
|
|
430
|
+
// navigation, which would sweep this away and \u2014 since it is never loaded
|
|
431
|
+
// twice \u2014 leave the document without it. These are loaded once and stay,
|
|
432
|
+
// the same as the external head scripts beside them.
|
|
433
|
+
el.setAttribute('data-stx-external','');
|
|
434
|
+
el.onload=function(){resolve()};
|
|
435
|
+
el.onerror=function(){log('[router] container script failed:',key);resolve()};
|
|
436
|
+
document.head.appendChild(el);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
383
440
|
// Record scripts from the initial page load
|
|
384
441
|
document.querySelectorAll('script').forEach(function(s){
|
|
385
442
|
var text=s.textContent||'';
|
|
386
|
-
if(
|
|
443
|
+
if(s.hasAttribute('src'))rememberExternalScript(s.getAttribute('src'));
|
|
444
|
+
else if(text.trim())executedScriptHashes[hashScript(text)]=1;
|
|
387
445
|
});
|
|
388
446
|
function cacheKey(url){
|
|
389
447
|
// Base is the CURRENT document, not the origin root: a relative key like
|
|
@@ -687,6 +745,7 @@ else {
|
|
|
687
745
|
var fragStyles=[];
|
|
688
746
|
var fragCrosswindHrefs=[];
|
|
689
747
|
var fragCrosswindCSS=null;
|
|
748
|
+
var fragExternalScripts=[];
|
|
690
749
|
var cleanFrag=html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/scr'+'ipt>','gi'),function(m,attrs,code){
|
|
691
750
|
// A data block is not code, and everything below this line assumes it
|
|
692
751
|
// is: the body gets stashed in fragScripts, re-emitted as a pending
|
|
@@ -704,6 +763,12 @@ else {
|
|
|
704
763
|
var typeMatch=attrs.match(/\\btype\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
705
764
|
var scriptType=((typeMatch&&(typeMatch[1]||typeMatch[2]||typeMatch[3]))||'').trim().toLowerCase();
|
|
706
765
|
if(scriptType&&scriptType!=='text/javascript'&&scriptType!=='application/javascript'&&scriptType!=='module')return m;
|
|
766
|
+
// An external script has no body, so every check below reads it as
|
|
767
|
+
// nothing to run and the fragment loses it. Queue it instead; the
|
|
768
|
+
// file is where the page's factory usually lives.
|
|
769
|
+
var srcMatch=attrs.match(/\\bsrc\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
|
|
770
|
+
var fragSrc=srcMatch&&(srcMatch[1]||srcMatch[2]||srcMatch[3]);
|
|
771
|
+
if(fragSrc){fragExternalScripts.push(fragSrc);return ''}
|
|
707
772
|
if(code&&code.trim()&&!isSignalsRuntimeScript({hasAttribute:function(name){return name==='data-stx-runtime'&&attrs.indexOf('data-stx-runtime')!==-1}},code)){
|
|
708
773
|
var slot='fragment-'+(++fragScriptId);
|
|
709
774
|
var scoped=/(?:^|\\s)data-stx-scoped(?:\\s|=|$)/i.test(attrs);
|
|
@@ -791,6 +856,7 @@ else {
|
|
|
791
856
|
log('[router] frag scripts:', fragScripts.length);
|
|
792
857
|
document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
|
|
793
858
|
fragScripts.sort(function(a,b){return (a.setupName?1:0)-(b.setupName?1:0)});
|
|
859
|
+
function runFragScripts(){
|
|
794
860
|
fragScripts.forEach(function(entry){
|
|
795
861
|
var code=entry.text;
|
|
796
862
|
log('[router] exec script len:', code.length, 'has __stx_setup:', code.indexOf('__stx_setup')>-1);
|
|
@@ -859,6 +925,20 @@ else {
|
|
|
859
925
|
log('[router] scripts done. _latestSetup:', !!(window.stx&&window.stx._latestSetup));
|
|
860
926
|
// THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
|
|
861
927
|
window.dispatchEvent(new Event('stx:load'));
|
|
928
|
+
}
|
|
929
|
+
// The container's own files first, for the same reason as the
|
|
930
|
+
// full-document path: the inline scripts call into them. Only awaited
|
|
931
|
+
// when something actually has to load, so a fragment with no external
|
|
932
|
+
// scripts \u2014 every fragment before this change \u2014 still runs its inline
|
|
933
|
+
// scripts in the same synchronous turn and nothing about its timing
|
|
934
|
+
// moves.
|
|
935
|
+
var fragPending=[];
|
|
936
|
+
fragExternalScripts.forEach(function(src){
|
|
937
|
+
var pending=loadExternalScript(src,url);
|
|
938
|
+
if(pending)fragPending.push(pending);
|
|
939
|
+
});
|
|
940
|
+
if(fragPending.length)Promise.all(fragPending).then(runFragScripts).catch(runFragScripts);
|
|
941
|
+
else runFragScripts();
|
|
862
942
|
}
|
|
863
943
|
return new Promise(function(resolve,reject){
|
|
864
944
|
function completeFragSwap(){
|
|
@@ -1015,12 +1095,15 @@ else {
|
|
|
1015
1095
|
// scripts as placeholders so re-execution keeps document.currentScript
|
|
1016
1096
|
// beside the component template it owns.
|
|
1017
1097
|
var routedBodyScripts=[];
|
|
1098
|
+
var routedExternalScripts=[];
|
|
1018
1099
|
var routedBodyScriptId=0;
|
|
1019
1100
|
function prepareRoutedBodyScripts(root){
|
|
1020
1101
|
if(!root)return;
|
|
1021
1102
|
root.querySelectorAll('script').forEach(function(s){
|
|
1022
1103
|
var text=s.textContent||'';
|
|
1023
|
-
|
|
1104
|
+
// Not discarded: queued against docBase, so a relative src resolves
|
|
1105
|
+
// against the page being navigated to rather than the current URL.
|
|
1106
|
+
if(s.hasAttribute('src')){routedExternalScripts.push(s.getAttribute('src'));s.remove();return}
|
|
1024
1107
|
var type=(s.getAttribute('type')||'').trim().toLowerCase();
|
|
1025
1108
|
var executable=!type||type==='text/javascript'||type==='application/javascript'||type==='module';
|
|
1026
1109
|
if(!executable||!text.trim())return;
|
|
@@ -1084,6 +1167,14 @@ else {
|
|
|
1084
1167
|
}));
|
|
1085
1168
|
});
|
|
1086
1169
|
|
|
1170
|
+
// Before the inline scripts, and through the same barrier: an inline
|
|
1171
|
+
// script's whole reason for existing is often to call into one of these,
|
|
1172
|
+
// so running it first would fail exactly the way this bug did.
|
|
1173
|
+
routedExternalScripts.forEach(function(src){
|
|
1174
|
+
var pending=loadExternalScript(src,docBase);
|
|
1175
|
+
if(pending)extPromises.push(pending);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1087
1178
|
// \u2500\u2500 Script re-execution \u2500\u2500
|
|
1088
1179
|
// Remove previously injected page scripts
|
|
1089
1180
|
document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
|
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.252",
|
|
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",
|