stx-router 0.2.250 → 0.2.251

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.
Files changed (2) hide show
  1. package/dist/client.js +93 -2
  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(text.trim()&&!s.hasAttribute('src'))executedScriptHashes[hashScript(text)]=1;
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
- if(s.hasAttribute('src')){s.remove();return}
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.250",
4
+ "version": "0.2.251",
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",