bun-plugin-stx 0.2.47 → 0.2.54

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.
@@ -0,0 +1,23 @@
1
+ // @bun
2
+ // ../router/src/layout-metadata.ts
3
+ function deriveLayoutGroup(layoutPath) {
4
+ const normalized = layoutPath.replace(/\\/g, "/");
5
+ const segments = normalized.split("/").filter(Boolean);
6
+ const layoutsIndex = segments.lastIndexOf("layouts");
7
+ const layoutSegment = layoutsIndex >= 0 ? segments[layoutsIndex + 1] : segments.at(-1);
8
+ if (!layoutSegment)
9
+ return "app";
10
+ return layoutSegment.replace(/\.stx$/i, "") || "app";
11
+ }
12
+ function extractLayoutMetadata(html) {
13
+ const layoutMeta = html.match(/<meta\b[^>]*name=["']stx-layout["'][^>]*content=["']([^"']+)["'][^>]*>/i);
14
+ const groupMeta = html.match(/<meta\b[^>]*name=["']stx-layout-group["'][^>]*content=["']([^"']+)["'][^>]*>/i);
15
+ const layoutComment = html.match(/<!--\s*stx-layout:\s*([^ ]+)\s*-->/i);
16
+ const layout = layoutMeta?.[1] || layoutComment?.[1] || "";
17
+ return {
18
+ layout,
19
+ group: groupMeta?.[1] || deriveLayoutGroup(layout)
20
+ };
21
+ }
22
+
23
+ export { deriveLayoutGroup, extractLayoutMetadata };
@@ -1,4 +1,8 @@
1
1
  // @bun
2
+ import {
3
+ deriveLayoutGroup,
4
+ extractLayoutMetadata
5
+ } from "./chunk-93ete56h.js";
2
6
  import"./chunk-2mx7fq49.js";
3
7
 
4
8
  // ../router/src/file-router.ts
@@ -585,6 +589,8 @@ function getRouterScript() {
585
589
  var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false};
586
590
  var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
587
591
  var containerSel=o.container;
592
+ var debug=!!o.debug;
593
+ function log(){if(debug&&typeof console!=='undefined'&&console.log)console.log.apply(console,arguments)}
588
594
 
589
595
  // \u2500\u2500 Progress bar (0\u2192100% at top of viewport) \u2500\u2500
590
596
  // Native loading indicator baked into the router. Starts when navigation
@@ -676,6 +682,7 @@ function getRouterScript() {
676
682
  // Layout group detection \u2014 layouts in the same group share a <main> container.
677
683
  // Only truly different layout groups trigger a full page reload.
678
684
  var layoutCache={};
685
+ var layoutGroupCache={};
679
686
  // Track executed script hashes to prevent redeclaration errors on navigation.
680
687
  // Layout-level scripts (theme, nav setup) execute on initial page load and
681
688
  // should NOT re-execute when navigating to another page with the same layout.
@@ -689,33 +696,54 @@ function getRouterScript() {
689
696
  var text=s.textContent||'';
690
697
  if(text.trim()&&!s.hasAttribute('src'))executedScriptHashes[hashScript(text)]=1;
691
698
  });
692
- function getLayoutGroup(layout){
699
+ function cacheKey(url){
700
+ var u=new URL(url,location.origin);
701
+ return u.pathname+u.search;
702
+ }
703
+ function defaultLayoutGroup(layout){
693
704
  if(!layout)return 'app';
694
- if(layout.indexOf('auth')!==-1||layout.indexOf('guest')!==-1)return 'auth';
695
- return 'app';
705
+ var clean=String(layout).replace(/\\\\/g,'/');
706
+ var parts=clean.split('/').filter(Boolean);
707
+ var idx=parts.lastIndexOf('layouts');
708
+ var part=idx>=0?parts[idx+1]:parts[parts.length-1];
709
+ return part?part.replace(/\\.stx$/i,''):'app';
710
+ }
711
+ function getCurrentLayoutGroup(){
712
+ var meta=document.querySelector('meta[name="stx-layout-group"]');
713
+ if(meta&&meta.getAttribute('content'))return meta.getAttribute('content');
714
+ var layout=document.querySelector('meta[name="stx-layout"]');
715
+ return defaultLayoutGroup(layout?layout.getAttribute('content'):'');
716
+ }
717
+ function getDocLayoutGroup(doc,layout){
718
+ var meta=doc&&doc.querySelector?doc.querySelector('meta[name="stx-layout-group"]'):null;
719
+ return meta&&meta.getAttribute('content')?meta.getAttribute('content'):defaultLayoutGroup(layout||'');
696
720
  }
697
- function checkLayoutChange(newLayout,targetUrl){
721
+ function checkLayoutChange(newLayout,targetUrl,newGroup){
698
722
  var currentLayout=document.querySelector('meta[name="stx-layout"]');
699
723
  var curLayoutName=currentLayout?currentLayout.getAttribute('content'):'';
700
- var curGroup=getLayoutGroup(curLayoutName);
701
- var newGroup=getLayoutGroup(newLayout);
702
- console.log('[router] layout check: current='+curLayoutName+'('+curGroup+') new='+(newLayout||'')+'('+newGroup+')');
724
+ var curGroup=getCurrentLayoutGroup();
725
+ var nextGroup=newGroup||defaultLayoutGroup(newLayout);
726
+ log('[router] layout check: current='+curLayoutName+'('+curGroup+') new='+(newLayout||'')+'('+nextGroup+')');
703
727
  // Different layout GROUP (e.g. app \u2192 auth): full reload
704
- if(curGroup!==newGroup){
705
- console.log('[router] layout group change:',curGroup,'\u2192',newGroup,'\u2014 full reload to:',targetUrl);
728
+ if(curGroup!==nextGroup){
729
+ log('[router] layout group change:',curGroup,'\u2192',nextGroup,'\u2014 full reload to:',targetUrl);
706
730
  return true;
707
731
  }
708
732
  // Same group but different SPECIFIC layout (e.g. layouts/app \u2192 layouts/coach):
709
733
  // full body swap so nav, sidebar, and other layout-level elements update
710
734
  if(curLayoutName && newLayout && curLayoutName!==newLayout){
711
- console.log('[router] layout name change:',curLayoutName,'\u2192',newLayout,'\u2014 full body swap');
735
+ log('[router] layout name change:',curLayoutName,'\u2192',newLayout,'\u2014 full body swap');
712
736
  return true;
713
737
  }
714
738
  return false;
715
739
  }
716
740
 
741
+ function shouldUseFragmentResponse(){
742
+ return containerSel==='main'||containerSel==='[data-stx-content]';
743
+ }
744
+
717
745
  function navigate(url,pushState,force){
718
- console.log('[router] navigate() called:',url,'isNavigating:',isNavigating);
746
+ log('[router] navigate() called:',url,'isNavigating:',isNavigating);
719
747
  if(isNavigating)return;
720
748
  var t=new URL(url,location.origin);
721
749
 
@@ -728,27 +756,33 @@ function getRouterScript() {
728
756
  return;
729
757
  }
730
758
 
731
- if(t.href===location.href&&!t.hash&&!force)return;
759
+ // Skip if user clicks a link to the page they're already on. We
760
+ // deliberately allow popstate (pushState===false) through because
761
+ // the browser has already updated location.href to the popped
762
+ // entry \u2014 without this allowance, hitting back would early-return
763
+ // and the visible page content would be left frozen on the
764
+ // forward-navigation page.
765
+ if(pushState!==false&&t.href===location.href&&!t.hash&&!force)return;
732
766
 
733
767
  isNavigating=true;
734
768
  document.body.classList.add(o.loadingClass);
735
769
  startProgress();
736
770
 
737
771
  var targetHref=t.href;
738
- var targetPath=t.pathname;
772
+ var targetPath=cacheKey(url);
739
773
  var targetHash=t.hash;
740
774
 
741
775
  function done(){isNavigating=false;document.body.classList.remove(o.loadingClass);finishProgress()}
742
776
 
743
777
  if(o.cache&&cache[targetPath]&&!force){
744
- if(checkLayoutChange(layoutCache[targetPath],url)){
778
+ if(checkLayoutChange(layoutCache[targetPath],url,layoutGroupCache[targetPath])){
745
779
  // Layout changed \u2014 fetch full page and do full document swap
746
- console.log('[router] cache hit but layout changed \u2014 fetching full page');
780
+ log('[router] cache hit but layout changed \u2014 fetching full page');
747
781
  fetch(url,{headers:{'Accept':'text/html'}}).then(function(r){
748
782
  if(!r.ok)throw new Error(r.status);
749
783
  return r.text();
750
784
  }).then(function(html){
751
- console.log('[router] full page fetched from cache path, len:',html.length);
785
+ log('[router] full page fetched from cache path, len:',html.length);
752
786
  swap(html,targetPath,pushState,targetHash);
753
787
  }).catch(function(err){
754
788
  console.error('[router] full page fetch error:',err);
@@ -761,28 +795,32 @@ function getRouterScript() {
761
795
  }
762
796
  else {
763
797
  if(force&&cache[targetPath])delete cache[targetPath];
764
- // First fetch as fragment (SPA mode)
765
- fetch(url,{headers:{'X-STX-Router':'true','Accept':'text/html'}}).then(function(r){
798
+ // First fetch as fragment (SPA mode) when the configured container is
799
+ // the page content. Custom app-shell containers need full documents so
800
+ // the router does not inject a <main> fragment into the wrong element.
801
+ var wantsFragment=shouldUseFragmentResponse();
802
+ fetch(url,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
766
803
  if(!r.ok)throw new Error(r.status);
767
- var isFragment=r.headers.get('X-STX-Fragment')==='true';
804
+ var isFragment=wantsFragment&&r.headers.get('X-STX-Fragment')==='true';
768
805
  var newLayout=r.headers.get('X-STX-Layout')||'';
806
+ var newGroup=r.headers.get('X-STX-Layout-Group')||'';
769
807
  // Layout change? Fetch the FULL page (no X-STX-Router header) and do full document swap
770
- if(isFragment&&checkLayoutChange(newLayout,url)){
771
- console.log('[router] layout change \u2014 fetching full page for document swap');
808
+ if(isFragment&&checkLayoutChange(newLayout,url,newGroup)){
809
+ log('[router] layout change \u2014 fetching full page for document swap');
772
810
  return fetch(url,{headers:{'Accept':'text/html'}}).then(function(fullRes){
773
- console.log('[router] full page fetched:',fullRes.status,'ok:',fullRes.ok);
811
+ log('[router] full page fetched:',fullRes.status,'ok:',fullRes.ok);
774
812
  if(!fullRes.ok)throw new Error(fullRes.status);
775
813
  return fullRes.text().then(function(html){
776
- console.log('[router] full page html length:',html.length);
777
- return{html:html,isFragment:false,layout:newLayout};
814
+ log('[router] full page html length:',html.length);
815
+ return{html:html,isFragment:false,layout:newLayout,layoutGroup:newGroup};
778
816
  });
779
817
  });
780
818
  }
781
- return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout}});
819
+ return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup}});
782
820
  }).then(function(result){
783
821
  if(!result)return;
784
822
  if(result.isFragment)result.html='<!--stx-fragment-->'+result.html;
785
- if(o.cache){cache[targetPath]=result.html;layoutCache[targetPath]=result.layout}
823
+ if(o.cache){cache[targetPath]=result.html;layoutCache[targetPath]=result.layout;layoutGroupCache[targetPath]=result.layoutGroup}
786
824
  swap(result.html,targetPath,pushState,targetHash);
787
825
  }).catch(function(err){
788
826
  console.error('[router] fetch error:',err);
@@ -795,8 +833,8 @@ else {
795
833
  var isFragment=html.indexOf('<!--stx-fragment-->')===0;
796
834
  if(isFragment)html=html.slice('<!--stx-fragment-->'.length);
797
835
  var currentContent=getContainer();
798
- console.log('[router] swap: isFragment='+isFragment+' container='+!!currentContent+' htmlLen='+html.length);
799
- if(!currentContent){console.log('[router] no container \u2014 falling back');location.href=url;return}
836
+ log('[router] swap: isFragment='+isFragment+' container='+!!currentContent+' tag='+(currentContent&&currentContent.tagName)+' selector='+containerSel+' htmlLen='+html.length);
837
+ if(!currentContent){log('[router] no container \u2014 falling back');location.href=url;return}
800
838
 
801
839
  // Fragment mode: server returned just the page content (no document wrapper)
802
840
  if(isFragment){
@@ -851,10 +889,22 @@ else {
851
889
  else if(hash){var el=document.querySelector(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
852
890
  window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
853
891
  // Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
854
- console.log('[router] frag scripts:', fragScripts.length);
892
+ log('[router] frag scripts:', fragScripts.length);
855
893
  document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
856
894
  fragScripts.forEach(function(code){
857
- console.log('[router] exec script len:', code.length, 'has __stx_setup:', code.indexOf('__stx_setup')>-1);
895
+ log('[router] exec script len:', code.length, 'has __stx_setup:', code.indexOf('__stx_setup')>-1);
896
+ // Skip scripts that were already executed (layout-level partials
897
+ // like theme.stx, stores.stx, nav.stx). Their top-level const/let
898
+ // declarations would throw "Identifier has already been declared"
899
+ // on re-execution. Setup functions (__stx_setup_) must always
900
+ // re-execute because each page has its own setup.
901
+ var h=hashScript(code);
902
+ var isSetup=code.indexOf('__stx_setup_')!==-1;
903
+ if(!isSetup&&executedScriptHashes[h]){
904
+ log('[router] skipping already-executed script (hash dedup)');
905
+ return;
906
+ }
907
+ executedScriptHashes[h]=1;
858
908
  var ns=document.createElement('script');
859
909
  // Wrap in block scope to prevent const/let collisions on re-navigation.
860
910
  // Without this, navigating away and back throws "Identifier has already
@@ -865,7 +915,7 @@ else {
865
915
  ns.setAttribute('data-stx-page','');
866
916
  document.body.appendChild(ns);
867
917
  });
868
- console.log('[router] scripts done. _latestSetup:', !!window.stx._latestSetup);
918
+ log('[router] scripts done. _latestSetup:', !!window.stx._latestSetup);
869
919
  // THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
870
920
  window.dispatchEvent(new Event('stx:load'));
871
921
  }
@@ -937,12 +987,14 @@ else {
937
987
  if(newBody){
938
988
  var newMeta=doc.querySelector('meta[name="stx-layout"]');
939
989
  var curMeta=document.querySelector('meta[name="stx-layout"]');
940
- var curGroup=getLayoutGroup(curMeta?curMeta.getAttribute('content'):'');
941
- var newGroup=getLayoutGroup(newMeta?newMeta.getAttribute('content'):'');
942
- isLayoutChange=curGroup!==newGroup;
990
+ var curLayout=curMeta?curMeta.getAttribute('content'):'';
991
+ var newLayout=newMeta?newMeta.getAttribute('content'):'';
992
+ var curGroup=getCurrentLayoutGroup();
993
+ var newGroup=getDocLayoutGroup(doc,newLayout);
994
+ isLayoutChange=curGroup!==newGroup||(curLayout&&newLayout&&curLayout!==newLayout);
943
995
  }
944
996
  if(isLayoutChange&&newBody){
945
- console.log('[router] full body swap for layout change');
997
+ log('[router] full body swap for layout change');
946
998
  // Replace entire body content \u2014 layout chrome and all
947
999
  var bodyHTML=newBody.innerHTML.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt\\\\s*>','gi'),'');
948
1000
  document.body.innerHTML=bodyHTML;
@@ -953,6 +1005,10 @@ else {
953
1005
  var freshMeta=doc.querySelector('meta[name="stx-layout"]');
954
1006
  if(oldMeta&&freshMeta)oldMeta.setAttribute('content',freshMeta.getAttribute('content')||'');
955
1007
  else if(freshMeta){var m=document.createElement('meta');m.name='stx-layout';m.content=freshMeta.getAttribute('content')||'';document.head.appendChild(m)}
1008
+ var oldGroupMeta=document.querySelector('meta[name="stx-layout-group"]');
1009
+ var freshGroupMeta=doc.querySelector('meta[name="stx-layout-group"]');
1010
+ if(oldGroupMeta&&freshGroupMeta)oldGroupMeta.setAttribute('content',freshGroupMeta.getAttribute('content')||'');
1011
+ else if(freshGroupMeta){var gm=document.createElement('meta');gm.name='stx-layout-group';gm.content=freshGroupMeta.getAttribute('content')||'';document.head.appendChild(gm)}
956
1012
  // Update container reference for script execution below
957
1013
  currentContent=document.querySelector(containerSel)||document.querySelector('main')||document.body;
958
1014
  } else {
@@ -983,6 +1039,9 @@ else {
983
1039
  document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
984
1040
 
985
1041
  var scripts=[];
1042
+ function addScript(text, runAlways){
1043
+ scripts.push({text:text,runAlways:!!runAlways});
1044
+ }
986
1045
  if(isLayoutChange){
987
1046
  // Layout change: collect ALL scripts from the new document body
988
1047
  // (layout scripts, component mounts, setup functions \u2014 everything)
@@ -995,7 +1054,7 @@ else {
995
1054
  // Skip the signals runtime IIFE \u2014 it's already loaded
996
1055
  if(text.indexOf("'use strict';var cloakStyle")!==-1)return;
997
1056
  if(text.indexOf('__stxRouter')!==-1)return;
998
- scripts.push(text);
1057
+ addScript(text,true);
999
1058
  });
1000
1059
  }
1001
1060
  // Also collect setup functions from <head>
@@ -1003,7 +1062,7 @@ else {
1003
1062
  var text=s.textContent||'';
1004
1063
  if(s.hasAttribute('src'))return;
1005
1064
  if(!text.trim())return;
1006
- if(text.indexOf('__stx_setup_')!==-1)scripts.push(text);
1065
+ if(text.indexOf('__stx_setup_')!==-1)addScript(text,true);
1007
1066
  });
1008
1067
  } else {
1009
1068
  // Same layout: collect scripts from the container
@@ -1011,26 +1070,27 @@ else {
1011
1070
  var text=s.textContent||'';
1012
1071
  if(s.hasAttribute('src'))return;
1013
1072
  if(!text.trim())return;
1014
- scripts.push(text);
1073
+ addScript(text,true);
1015
1074
  });
1016
- // Collect setup scripts from <head> AND <body> (outside container).
1075
+ // Collect page scripts from <head> AND <body> (outside container).
1017
1076
  // SSG builds place the setup function in <body> before <main>, not
1018
1077
  // in <head>. Without this, SPA navigation on static sites never
1019
1078
  // runs the setup and reactive data stays empty.
1020
1079
  var seenSetups={};
1021
- scripts.forEach(function(t){if(t.indexOf('__stx_setup_')!==-1)seenSetups[t.substring(0,80)]=1});
1080
+ scripts.forEach(function(entry){var t=entry.text;if(t.indexOf('__stx_setup_')!==-1)seenSetups[t.substring(0,80)]=1});
1022
1081
  doc.querySelectorAll('script').forEach(function(s){
1023
1082
  var text=s.textContent||'';
1024
1083
  if(s.hasAttribute('src'))return;
1025
1084
  if(!text.trim())return;
1026
- if(text.indexOf('__stx_setup_')===-1)return;
1027
1085
  // Skip signals runtime and router (they contain __stx_setup references but aren't setup functions)
1028
1086
  if(text.indexOf("'use strict';var cloakStyle")!==-1)return;
1029
1087
  if(text.indexOf('__stxRouter')!==-1)return;
1088
+ if(newContent.contains(s))return;
1089
+ if(text.indexOf('__stx_setup_')===-1&&!s.hasAttribute('data-stx-scoped')&&!s.hasAttribute('data-stx-page'))return;
1030
1090
  var key=text.substring(0,80);
1031
1091
  if(seenSetups[key])return;
1032
1092
  seenSetups[key]=1;
1033
- scripts.push(text);
1093
+ addScript(text,true);
1034
1094
  });
1035
1095
  }
1036
1096
 
@@ -1060,7 +1120,9 @@ else {
1060
1120
 
1061
1121
  // Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
1062
1122
  function execScripts(){
1063
- scripts.forEach(function(text){
1123
+ scripts.forEach(function(entry){
1124
+ var text=typeof entry==='string'?entry:entry.text;
1125
+ var runAlways=typeof entry==='string'?false:entry.runAlways;
1064
1126
  // Skip scripts that were already executed (layout-level partials
1065
1127
  // like theme.stx). Their top-level const/function declarations
1066
1128
  // would throw "Identifier has already been declared" on re-execution.
@@ -1070,7 +1132,7 @@ else {
1070
1132
  var h=hashScript(text);
1071
1133
  var isSetup=text.indexOf('__stx_setup_')!==-1;
1072
1134
  var hasImport=text.indexOf('import ')!==-1;
1073
- if(!isSetup&&executedScriptHashes[h])return;
1135
+ if(!runAlways&&!isSetup&&executedScriptHashes[h])return;
1074
1136
  executedScriptHashes[h]=1;
1075
1137
  // Wrap scripts with import statements as modules (Bug 3 fix)
1076
1138
  var ns=document.createElement('script');
@@ -1140,12 +1202,12 @@ else {
1140
1202
  }
1141
1203
  if(!link){return}
1142
1204
  var href=link.getAttribute('href');
1143
- console.log('[router] click intercepted:',href,'container:',!!getContainer(),'defaultPrevented:',e.defaultPrevented);
1144
- if(!href||href.startsWith('http')||href.startsWith('#')||href.startsWith('mailto:')||href.startsWith('tel:')){console.log('[router] skipped:',href);return}
1205
+ log('[router] click intercepted:',href,'container:',!!getContainer(),'defaultPrevented:',e.defaultPrevented);
1206
+ if(!href||href.startsWith('http')||href.startsWith('#')||href.startsWith('mailto:')||href.startsWith('tel:')){log('[router] skipped:',href);return}
1145
1207
  if(link.target==='_blank'||link.hasAttribute('download'))return;
1146
1208
  e.preventDefault();
1147
1209
  e.stopPropagation();
1148
- console.log('[router] navigating to:',href);
1210
+ log('[router] navigating to:',href);
1149
1211
  navigate(href);
1150
1212
  },true);
1151
1213
 
@@ -1161,15 +1223,18 @@ else {
1161
1223
  var link=e.target.closest('[data-stx-link]');
1162
1224
  if(!link)return;
1163
1225
  var href=link.getAttribute('href');
1164
- if(cache[href]||prefetching[href])return;
1165
- prefetching[href]=true;
1166
- fetch(href,{headers:{'X-STX-Router':'true','Accept':'text/html'}}).then(function(r){
1167
- var isFrag=r.headers.get('X-STX-Fragment')==='true';
1226
+ var key=cacheKey(href);
1227
+ if(cache[key]||prefetching[key])return;
1228
+ prefetching[key]=true;
1229
+ var wantsFragment=shouldUseFragmentResponse();
1230
+ fetch(href,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
1231
+ var isFrag=wantsFragment&&r.headers.get('X-STX-Fragment')==='true';
1168
1232
  var pLayout=r.headers.get('X-STX-Layout')||'';
1169
- return r.text().then(function(html){return{html:isFrag?'<!--stx-fragment-->'+html:html,layout:pLayout}});
1233
+ var pGroup=r.headers.get('X-STX-Layout-Group')||'';
1234
+ return r.text().then(function(html){return{html:isFrag?'<!--stx-fragment-->'+html:html,layout:pLayout,layoutGroup:pGroup}});
1170
1235
  }).then(function(result){
1171
- if(o.cache){cache[href]=result.html;layoutCache[href]=result.layout}
1172
- }).catch(function(){}).finally(function(){delete prefetching[href]});
1236
+ if(o.cache){cache[key]=result.html;layoutCache[key]=result.layout;layoutGroupCache[key]=result.layoutGroup}
1237
+ }).catch(function(){}).finally(function(){delete prefetching[key]});
1173
1238
  },true);
1174
1239
  }
1175
1240
 
@@ -1262,11 +1327,13 @@ else {
1262
1327
  navigate:navigate,
1263
1328
  navigateTo:navigate,
1264
1329
  prefetch:function(url){
1265
- if(!cache[url]){
1266
- fetch(url,{headers:{'X-STX-Router':'true'}}).then(function(r){var isFrag=r.headers.get('X-STX-Fragment')==='true';return r.text().then(function(html){return isFrag?'<!--stx-fragment-->'+html:html})}).then(function(html){cache[url]=html}).catch(function(){});
1330
+ var key=cacheKey(url);
1331
+ if(!cache[key]){
1332
+ var wantsFragment=shouldUseFragmentResponse();
1333
+ fetch(url,{headers:wantsFragment?{'X-STX-Router':'true'}:{'Accept':'text/html'}}).then(function(r){var isFrag=wantsFragment&&r.headers.get('X-STX-Fragment')==='true';var pLayout=r.headers.get('X-STX-Layout')||'';var pGroup=r.headers.get('X-STX-Layout-Group')||'';return r.text().then(function(html){return{html:isFrag?'<!--stx-fragment-->'+html:html,layout:pLayout,layoutGroup:pGroup}})}).then(function(result){cache[key]=result.html;layoutCache[key]=result.layout;layoutGroupCache[key]=result.layoutGroup}).catch(function(){});
1267
1334
  }
1268
1335
  },
1269
- clearCache:function(){for(var k in cache)delete cache[k]},
1336
+ clearCache:function(){for(var k in cache)delete cache[k];for(var lk in layoutCache)delete layoutCache[lk];for(var gk in layoutGroupCache)delete layoutGroupCache[gk]},
1270
1337
  cache:cache,
1271
1338
  swap:swap,
1272
1339
  updateNav:updateNav
@@ -1308,6 +1375,8 @@ export {
1308
1375
  formatRoutes,
1309
1376
  findErrorPage,
1310
1377
  filePathToPattern,
1378
+ extractLayoutMetadata,
1379
+ deriveLayoutGroup,
1311
1380
  defineRoutes,
1312
1381
  defineRoute,
1313
1382
  defineMiddleware,
package/dist/serve.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ import {
4
+ deriveLayoutGroup
5
+ } from "./chunk-93ete56h.js";
3
6
  import {
4
7
  __require
5
8
  } from "./chunk-2mx7fq49.js";
@@ -8956,7 +8959,7 @@ After installing: bun add bun-plugin-stx`);
8956
8959
  pageMiddlewarePatterns.length = 0;
8957
8960
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
8958
8961
  try {
8959
- const { Router } = await import("./chunk-mf3agv0w.js");
8962
+ const { Router } = await import("./chunk-nznv3e0m.js");
8960
8963
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
8961
8964
  const router = new Router(process19.cwd(), { pagesDirs });
8962
8965
  console.log(`[stx] Generated ${router.routes.length} routes \u2192 .stx/routes.ts`);
@@ -9540,6 +9543,7 @@ ${output}`;
9540
9543
  if (isSpaNav) {
9541
9544
  const layoutMatch = content.match(/<!-- stx-layout: ([^ ]+) -->/);
9542
9545
  const pageLayout = layoutMatch ? layoutMatch[1] : "default";
9546
+ const pageLayoutGroup = deriveLayoutGroup(pageLayout);
9543
9547
  let fragment = content;
9544
9548
  const headStyles = [];
9545
9549
  const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
@@ -9608,6 +9612,7 @@ ${pageSetupScripts.join(`
9608
9612
  "Content-Type": "text/html; charset=utf-8",
9609
9613
  "X-STX-Fragment": "true",
9610
9614
  "X-STX-Layout": pageLayout,
9615
+ "X-STX-Layout-Group": pageLayoutGroup,
9611
9616
  "Cache-Control": "no-cache",
9612
9617
  ...corsHeaders
9613
9618
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.47",
4
+ "version": "0.2.54",
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",
@@ -43,8 +43,9 @@
43
43
  "import": "./dist/serve.js"
44
44
  },
45
45
  "./*": {
46
- "bun": "./dist/*",
47
- "import": "./dist/*"
46
+ "bun": "./dist/*.js",
47
+ "types": "./dist/*.d.ts",
48
+ "import": "./dist/*.js"
48
49
  }
49
50
  },
50
51
  "module": "./dist/index.js",
@@ -60,18 +61,19 @@
60
61
  ],
61
62
  "scripts": {
62
63
  "build": "bun --bun build.ts",
63
- "lint": "bunx --bun pickier lint",
64
- "lint:fix": "bunx --bun pickier lint --fix",
65
- "fresh": "bunx rimraf node_modules/ bun.lock && bun i",
66
- "changelog": "bunx changelogen --output CHANGELOG.md",
64
+ "lint": "bun x --bun pickier lint",
65
+ "lint:fix": "bun x --bun pickier lint --fix",
66
+ "fresh": "bun x rimraf node_modules/ bun.lock && bun i",
67
+ "changelog": "bun x changelogen --output CHANGELOG.md",
67
68
  "prepublishOnly": "bun run build",
68
- "release": "bun run changelog && bunx bumpp package.json --all",
69
+ "release": "bun run changelog && bun x bumpp package.json --all",
69
70
  "test": "bun test --preload ../../test-utils/happy-dom.ts",
70
71
  "test:watch": "bun test --watch --preload ../../test-utils/happy-dom.ts",
71
72
  "test:coverage": "bun test --coverage --preload ../../test-utils/happy-dom.ts",
72
73
  "typecheck": "bun tsc --noEmit"
73
74
  },
74
75
  "dependencies": {
75
- "@stacksjs/stx": "0.2.47"
76
+ "@stacksjs/stx": "0.2.54",
77
+ "stx-router": "0.2.53"
76
78
  }
77
79
  }