bun-plugin-stx 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.
@@ -1011,10 +1011,68 @@ function getRouterScript() {
1011
1011
  function fragmentMarker(declared){
1012
1012
  return '<!--stx-fragment'+(declared==='true'?' rt=1':declared==='false'?' rt=0':'')+'-->';
1013
1013
  }
1014
+ // \u2500\u2500 External scripts inside the routed container \u2500\u2500
1015
+ //
1016
+ // A page whose x-data factory lives in its own file \u2014 a
1017
+ // <script src="dashboard-xdata.js"> sitting inside [data-stx-content] \u2014 lost that
1018
+ // file on every SPA navigation. Both swap paths dropped it: the fragment
1019
+ // path's regex sees an empty body and returns '', and the full-document path
1020
+ // removed it outright. The inline scripts beside it were re-executed with
1021
+ // care; the external ones were deleted.
1022
+ //
1023
+ // The result was a screen that rendered on a cold load and came back empty
1024
+ // after a click, reporting "dashboardXData is not defined" with every binding
1025
+ // in the container left unevaluated \u2014 because nothing had defined it since
1026
+ // the file was thrown away.
1027
+ //
1028
+ // Loaded once per URL, never re-run. What these files define is a global that
1029
+ // outlives the navigation, so a second execution buys nothing and costs
1030
+ // whatever side effects the file has: a mount helper firing again on every
1031
+ // click through the same page is its own bug.
1032
+ var loadedExternalScripts={};
1033
+ // The base is resolved first because callers pass what they have: the
1034
+ // fragment path knows only the path it navigated to ('/reports/disk'), and
1035
+ // new URL(src, '/reports/disk') throws \u2014 a base must be absolute. Silently
1036
+ // returning '' there meant the file was never requested, which is the bug
1037
+ // this whole block exists to fix, reintroduced one level down.
1038
+ function externalScriptKey(src,base){
1039
+ try{
1040
+ var absoluteBase=base?new URL(base,location.href).href:location.href;
1041
+ return new URL(src,absoluteBase).href;
1042
+ }catch(e){return ''}
1043
+ }
1044
+ function rememberExternalScript(src,base){
1045
+ var key=externalScriptKey(src,base);
1046
+ if(key)loadedExternalScripts[key]=1;
1047
+ return key;
1048
+ }
1049
+ // Returns a promise, or null when there is nothing to wait for \u2014 an already
1050
+ // loaded file, or an unparseable src. Resolves rather than rejects on error:
1051
+ // one 404 must not stop the rest of the page hydrating, matching what the
1052
+ // head-script path already does by sending failures to execScripts too.
1053
+ function loadExternalScript(src,base){
1054
+ var key=externalScriptKey(src,base);
1055
+ if(!key||loadedExternalScripts[key])return null;
1056
+ loadedExternalScripts[key]=1;
1057
+ return new Promise(function(resolve){
1058
+ var el=document.createElement('script');
1059
+ el.src=key;
1060
+ // Deliberately NOT data-stx-page: both swap paths clear those on every
1061
+ // navigation, which would sweep this away and \u2014 since it is never loaded
1062
+ // twice \u2014 leave the document without it. These are loaded once and stay,
1063
+ // the same as the external head scripts beside them.
1064
+ el.setAttribute('data-stx-external','');
1065
+ el.onload=function(){resolve()};
1066
+ el.onerror=function(){log('[router] container script failed:',key);resolve()};
1067
+ document.head.appendChild(el);
1068
+ });
1069
+ }
1070
+
1014
1071
  // Record scripts from the initial page load
1015
1072
  document.querySelectorAll('script').forEach(function(s){
1016
1073
  var text=s.textContent||'';
1017
- if(text.trim()&&!s.hasAttribute('src'))executedScriptHashes[hashScript(text)]=1;
1074
+ if(s.hasAttribute('src'))rememberExternalScript(s.getAttribute('src'));
1075
+ else if(text.trim())executedScriptHashes[hashScript(text)]=1;
1018
1076
  });
1019
1077
  function cacheKey(url){
1020
1078
  // Base is the CURRENT document, not the origin root: a relative key like
@@ -1318,6 +1376,7 @@ else {
1318
1376
  var fragStyles=[];
1319
1377
  var fragCrosswindHrefs=[];
1320
1378
  var fragCrosswindCSS=null;
1379
+ var fragExternalScripts=[];
1321
1380
  var cleanFrag=html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/scr'+'ipt>','gi'),function(m,attrs,code){
1322
1381
  // A data block is not code, and everything below this line assumes it
1323
1382
  // is: the body gets stashed in fragScripts, re-emitted as a pending
@@ -1335,6 +1394,12 @@ else {
1335
1394
  var typeMatch=attrs.match(/\\btype\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
1336
1395
  var scriptType=((typeMatch&&(typeMatch[1]||typeMatch[2]||typeMatch[3]))||'').trim().toLowerCase();
1337
1396
  if(scriptType&&scriptType!=='text/javascript'&&scriptType!=='application/javascript'&&scriptType!=='module')return m;
1397
+ // An external script has no body, so every check below reads it as
1398
+ // nothing to run and the fragment loses it. Queue it instead; the
1399
+ // file is where the page's factory usually lives.
1400
+ var srcMatch=attrs.match(/\\bsrc\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
1401
+ var fragSrc=srcMatch&&(srcMatch[1]||srcMatch[2]||srcMatch[3]);
1402
+ if(fragSrc){fragExternalScripts.push(fragSrc);return ''}
1338
1403
  if(code&&code.trim()&&!isSignalsRuntimeScript({hasAttribute:function(name){return name==='data-stx-runtime'&&attrs.indexOf('data-stx-runtime')!==-1}},code)){
1339
1404
  var slot='fragment-'+(++fragScriptId);
1340
1405
  var scoped=/(?:^|\\s)data-stx-scoped(?:\\s|=|$)/i.test(attrs);
@@ -1422,6 +1487,7 @@ else {
1422
1487
  log('[router] frag scripts:', fragScripts.length);
1423
1488
  document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
1424
1489
  fragScripts.sort(function(a,b){return (a.setupName?1:0)-(b.setupName?1:0)});
1490
+ function runFragScripts(){
1425
1491
  fragScripts.forEach(function(entry){
1426
1492
  var code=entry.text;
1427
1493
  log('[router] exec script len:', code.length, 'has __stx_setup:', code.indexOf('__stx_setup')>-1);
@@ -1490,6 +1556,20 @@ else {
1490
1556
  log('[router] scripts done. _latestSetup:', !!(window.stx&&window.stx._latestSetup));
1491
1557
  // THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
1492
1558
  window.dispatchEvent(new Event('stx:load'));
1559
+ }
1560
+ // The container's own files first, for the same reason as the
1561
+ // full-document path: the inline scripts call into them. Only awaited
1562
+ // when something actually has to load, so a fragment with no external
1563
+ // scripts \u2014 every fragment before this change \u2014 still runs its inline
1564
+ // scripts in the same synchronous turn and nothing about its timing
1565
+ // moves.
1566
+ var fragPending=[];
1567
+ fragExternalScripts.forEach(function(src){
1568
+ var pending=loadExternalScript(src,url);
1569
+ if(pending)fragPending.push(pending);
1570
+ });
1571
+ if(fragPending.length)Promise.all(fragPending).then(runFragScripts).catch(runFragScripts);
1572
+ else runFragScripts();
1493
1573
  }
1494
1574
  return new Promise(function(resolve,reject){
1495
1575
  function completeFragSwap(){
@@ -1646,12 +1726,15 @@ else {
1646
1726
  // scripts as placeholders so re-execution keeps document.currentScript
1647
1727
  // beside the component template it owns.
1648
1728
  var routedBodyScripts=[];
1729
+ var routedExternalScripts=[];
1649
1730
  var routedBodyScriptId=0;
1650
1731
  function prepareRoutedBodyScripts(root){
1651
1732
  if(!root)return;
1652
1733
  root.querySelectorAll('script').forEach(function(s){
1653
1734
  var text=s.textContent||'';
1654
- if(s.hasAttribute('src')){s.remove();return}
1735
+ // Not discarded: queued against docBase, so a relative src resolves
1736
+ // against the page being navigated to rather than the current URL.
1737
+ if(s.hasAttribute('src')){routedExternalScripts.push(s.getAttribute('src'));s.remove();return}
1655
1738
  var type=(s.getAttribute('type')||'').trim().toLowerCase();
1656
1739
  var executable=!type||type==='text/javascript'||type==='application/javascript'||type==='module';
1657
1740
  if(!executable||!text.trim())return;
@@ -1715,6 +1798,14 @@ else {
1715
1798
  }));
1716
1799
  });
1717
1800
 
1801
+ // Before the inline scripts, and through the same barrier: an inline
1802
+ // script's whole reason for existing is often to call into one of these,
1803
+ // so running it first would fail exactly the way this bug did.
1804
+ routedExternalScripts.forEach(function(src){
1805
+ var pending=loadExternalScript(src,docBase);
1806
+ if(pending)extPromises.push(pending);
1807
+ });
1808
+
1718
1809
  // \u2500\u2500 Script re-execution \u2500\u2500
1719
1810
  // Remove previously injected page scripts
1720
1811
  document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
package/dist/serve.js CHANGED
@@ -9592,7 +9592,7 @@ function __stxOverlay(errs){
9592
9592
  pageMiddlewarePatterns.length = 0;
9593
9593
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9594
9594
  try {
9595
- const { Router } = await import("./chunk-3d72end9.js");
9595
+ const { Router } = await import("./chunk-xg4124d4.js");
9596
9596
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9597
9597
  const router = new Router(process19.cwd(), { pagesDirs, stateDir: stateDirName() });
9598
9598
  if (!options.quiet)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.250",
4
+ "version": "0.2.251",
5
5
  "description": "A Bun plugin that allows for using Laravel Blade-like syntax.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",