bun-plugin-stx 0.2.110 → 0.2.112

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.
@@ -657,24 +657,65 @@ function getRouterScript() {
657
657
  // rather than a Map to keep the diff localized and the runtime
658
658
  // hand-minified shape consistent.
659
659
  var cacheOrder=[];
660
- function setCache(key,html,layout,group,title){
660
+ function setCache(key,html,layout,group,title,cattrs){
661
661
  if(!(key in cache))cacheOrder.push(key);
662
662
  cache[key]=html;
663
663
  layoutCache[key]=layout;
664
664
  layoutGroupCache[key]=group;
665
665
  titleCache[key]=title||'';
666
+ attrsCache[key]=cattrs||'';
666
667
  while(cacheOrder.length>o.prefetchCacheMax){
667
668
  var oldest=cacheOrder.shift();
668
669
  delete cache[oldest];
669
670
  delete layoutCache[oldest];
670
671
  delete layoutGroupCache[oldest];
671
672
  delete titleCache[oldest];
673
+ delete attrsCache[oldest];
672
674
  }
673
675
  }
674
676
  // Apply a page title captured from the fragment response's X-STX-Title
675
677
  // header (URI-encoded) so SPA swaps keep document.title in sync with the
676
678
  // page \u2014 full-document swaps set the title from the <title> tag directly.
677
679
  function applyTitle(t){if(t){try{document.title=decodeURIComponent(t)}catch(e){document.title=t}}}
680
+ // \u2500\u2500 Container attributes \u2500\u2500
681
+ // A fragment carries only the container's INNER content, so any layout that
682
+ // lives on the destination's <main> itself (e.g. a centered auth page whose
683
+ // <main> carries flex + min-h-[100dvh] + items-center) would be
684
+ // dropped when injected into the persistent container. The server sends the
685
+ // destination container's attributes in X-STX-Container-Attrs (URI-encoded);
686
+ // we apply them here and track what we applied so navigating onward to a page
687
+ // with a bare <main> clears them again. Attributes the router never applied
688
+ // (the app's own id/data-stx-content hooks) are left untouched.
689
+ var pendingContainerAttrs='';
690
+ function parseContainerAttrs(attrStr){
691
+ var map={};
692
+ if(!attrStr)return map;
693
+ var probe=document.createElement('div');
694
+ probe.innerHTML='<i '+attrStr+'></i>';
695
+ var src=probe.firstChild;
696
+ if(src&&src.attributes){
697
+ Array.prototype.forEach.call(src.attributes,function(a){map[a.name]=a.value});
698
+ }
699
+ return map;
700
+ }
701
+ function setContainerAttrs(el,map){
702
+ if(!el)return;
703
+ var prev=(el.getAttribute('data-stx-cattrs')||'').split(',');
704
+ prev.forEach(function(n){if(n&&!(n in map))el.removeAttribute(n)});
705
+ var names=[];
706
+ for(var k in map){
707
+ if(Object.prototype.hasOwnProperty.call(map,k)&&k!=='data-stx-cattrs'){
708
+ el.setAttribute(k,map[k]);names.push(k);
709
+ }
710
+ }
711
+ if(names.length)el.setAttribute('data-stx-cattrs',names.join(','));
712
+ else el.removeAttribute('data-stx-cattrs');
713
+ }
714
+ function applyContainerAttrs(el,encoded){
715
+ var attrStr='';
716
+ if(encoded){try{attrStr=decodeURIComponent(encoded)}catch(e){attrStr=encoded}}
717
+ setContainerAttrs(el,parseContainerAttrs(attrStr));
718
+ }
678
719
  function touchCache(key){
679
720
  var i=cacheOrder.indexOf(key);
680
721
  if(i>=0){cacheOrder.splice(i,1);cacheOrder.push(key)}
@@ -686,6 +727,7 @@ function getRouterScript() {
686
727
  delete layoutCache[key];
687
728
  delete layoutGroupCache[key];
688
729
  delete titleCache[key];
730
+ delete attrsCache[key];
689
731
  }
690
732
 
691
733
  // Extract top-level CSS blocks (rules AND @media blocks with nested braces).
@@ -739,6 +781,7 @@ function getRouterScript() {
739
781
  var layoutCache={};
740
782
  var layoutGroupCache={};
741
783
  var titleCache={};
784
+ var attrsCache={};
742
785
  // Track executed script hashes to prevent redeclaration errors on navigation.
743
786
  // Layout-level scripts (theme, nav setup) execute on initial page load and
744
787
  // should NOT re-execute when navigating to another page with the same layout.
@@ -753,7 +796,10 @@ function getRouterScript() {
753
796
  if(text.trim()&&!s.hasAttribute('src'))executedScriptHashes[hashScript(text)]=1;
754
797
  });
755
798
  function cacheKey(url){
756
- var u=new URL(url,location.origin);
799
+ // Base is the CURRENT document, not the origin root: a relative key like
800
+ // '?status=resolved' on /dashboard must cache as /dashboard?status=resolved,
801
+ // not /?status=resolved (#1777).
802
+ var u=new URL(url,location.href);
757
803
  return u.pathname+u.search;
758
804
  }
759
805
  // Keep SPA fetches inside the active locale (/en/... when viewing English).
@@ -810,7 +856,12 @@ function getRouterScript() {
810
856
  if(!force) url=withCurrentLocale(url);
811
857
  log('[router] navigate() called:',url,'isNavigating:',isNavigating);
812
858
  if(isNavigating)return;
813
- var t=new URL(url,location.origin);
859
+ // Resolve against the current document URL, matching native <a> semantics.
860
+ // With location.origin as the base, any relative href ('?status=resolved',
861
+ // '#anchor', './sibling') resolved to the SITE ROOT \u2014 so filter tabs,
862
+ // pagination and sort links silently bounced users to the landing page once
863
+ // the SPA interceptor was active (#1777).
864
+ var t=new URL(url,location.href);
814
865
 
815
866
  if(t.origin!==location.origin){location.href=url;return}
816
867
 
@@ -858,6 +909,7 @@ function getRouterScript() {
858
909
  }).finally(done);
859
910
  return;
860
911
  }
912
+ pendingContainerAttrs=attrsCache[targetPath]||'';
861
913
  swap(cache[targetPath],targetPath,pushState,targetHash);
862
914
  applyTitle(titleCache[targetPath]);
863
915
  done();
@@ -874,6 +926,7 @@ else {
874
926
  var newLayout=r.headers.get('X-STX-Layout')||'';
875
927
  var newGroup=r.headers.get('X-STX-Layout-Group')||'';
876
928
  var newTitle=r.headers.get('X-STX-Title')||'';
929
+ var newCAttrs=r.headers.get('X-STX-Container-Attrs')||'';
877
930
  // Layout change? Fetch the FULL page (no X-STX-Router header) and do full document swap
878
931
  if(isFragment&&checkLayoutChange(newLayout,url,newGroup)){
879
932
  log('[router] layout change \u2014 fetching full page for document swap');
@@ -886,11 +939,12 @@ else {
886
939
  });
887
940
  });
888
941
  }
889
- return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle}});
942
+ return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle,containerAttrs:newCAttrs}});
890
943
  }).then(function(result){
891
944
  if(!result)return;
892
945
  if(result.isFragment)result.html='<!--stx-fragment-->'+result.html;
893
- if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title);
946
+ if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
947
+ pendingContainerAttrs=result.isFragment?(result.containerAttrs||''):'';
894
948
  swap(result.html,targetPath,pushState,targetHash);
895
949
  // Fragment swaps carry no <head>; apply the title from the header.
896
950
  // Full-document swaps already set document.title from the <title> tag.
@@ -963,7 +1017,10 @@ else {
963
1017
  el.setAttribute('data-stx-page','');
964
1018
  document.head.appendChild(el);
965
1019
  });
966
- // Swap content
1020
+ // Swap content \u2014 apply the destination container's own attributes first
1021
+ // so the incoming markup lands in a correctly-laid-out container instead
1022
+ // of flashing (or sticking) unstyled.
1023
+ applyContainerAttrs(currentContent,pendingContainerAttrs);
967
1024
  currentContent.innerHTML=cleanFrag;
968
1025
  // Remove old page scripts
969
1026
  document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
@@ -1074,6 +1131,35 @@ else {
1074
1131
 
1075
1132
  incoming.forEach(function(s){s.removeAttribute('data-stx-incoming')});
1076
1133
 
1134
+ // \u2500\u2500 Swap <head> stylesheet / font <link>s \u2500\u2500
1135
+ // Without this, an SPA navigation to a page whose <link rel="stylesheet">
1136
+ // (or font/preconnect <link>) the current page had not already loaded left
1137
+ // the swapped-in content completely unstyled (e.g. a page that pulls in its
1138
+ // own marketing.css, or Google Fonts). Additive and href-deduped, mirroring
1139
+ // the external <head script[src]> reconcile below \u2014 never removes existing
1140
+ // links, so stylesheets shared across pages simply persist.
1141
+ // Relative asset hrefs resolve against the document that declared them:
1142
+ // the CURRENT page for links already in <head>, and the page being
1143
+ // navigated TO for links from the fetched document. Against location.origin
1144
+ // both collapsed to the site root, so 'assets/app.css' on /docs/intro
1145
+ // deduped as /assets/app.css \u2014 the wrong file, and a false cache hit that
1146
+ // skipped the real stylesheet (#1777).
1147
+ var docBase;
1148
+ try{docBase=new URL(url,location.href).href}catch(e){docBase=location.href}
1149
+ var linkSel='link[rel="stylesheet"],link[rel="preconnect"]';
1150
+ var curLinks={};
1151
+ document.querySelectorAll('head '+linkSel).forEach(function(l){var h=l.getAttribute('href');if(h)curLinks[new URL(h,location.href).href]=1});
1152
+ doc.querySelectorAll('head '+linkSel).forEach(function(l){
1153
+ var href=l.getAttribute('href');
1154
+ if(!href)return;
1155
+ var abs=new URL(href,docBase).href;
1156
+ if(curLinks[abs])return;
1157
+ curLinks[abs]=1;
1158
+ var nl=document.createElement('link');
1159
+ Array.from(l.attributes).forEach(function(a){nl.setAttribute(a.name,a.value)});
1160
+ document.head.appendChild(nl);
1161
+ });
1162
+
1077
1163
  // \u2500\u2500 Swap content \u2500\u2500
1078
1164
  // For layout changes: swap the entire <body> to replace layout chrome (nav, footer, etc.)
1079
1165
  // For same-layout: swap only the container (<main>)
@@ -1107,7 +1193,12 @@ else {
1107
1193
  // Update container reference for script execution below
1108
1194
  currentContent=document.querySelector(containerSel)||document.querySelector('main')||document.body;
1109
1195
  } else {
1110
- // Same layout \u2014 swap only container content
1196
+ // Same layout \u2014 swap only container content. Carry the destination
1197
+ // container's attributes across too (same reason as the fragment path:
1198
+ // pages whose layout lives on <main> itself would otherwise lose it).
1199
+ var attrMap={};
1200
+ Array.prototype.forEach.call(newContent.attributes,function(a){attrMap[a.name]=a.value});
1201
+ setContainerAttrs(currentContent,attrMap);
1111
1202
  var cleanHTML=newContent.innerHTML.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt\\\\s*>','gi'),'');
1112
1203
  currentContent.innerHTML=cleanHTML;
1113
1204
  }
@@ -1117,7 +1208,9 @@ else {
1117
1208
  document.querySelectorAll('head script[src]').forEach(function(s){loadedSrcs[s.src]=1});
1118
1209
  var extPromises=[];
1119
1210
  doc.querySelectorAll('head script[src]').forEach(function(s){
1120
- var src=new URL(s.getAttribute('src'),location.origin).href;
1211
+ // Fetched-document script: resolve against the page being navigated to
1212
+ // (docBase), not the origin root \u2014 same rationale as the <link> reconcile (#1777).
1213
+ var src=new URL(s.getAttribute('src'),docBase).href;
1121
1214
  if(loadedSrcs[src])return;
1122
1215
  loadedSrcs[src]=1;
1123
1216
  extPromises.push(new Promise(function(resolve,reject){
package/dist/serve.js CHANGED
@@ -8702,6 +8702,7 @@ var defaultGeneratedDir4 = resolve12(process20.cwd(), "src/generated");
8702
8702
  // src/serve.ts
8703
8703
  import { stateDir, stateDirName } from "@stacksjs/stx";
8704
8704
  var defaultStxModule = import("@stacksjs/stx");
8705
+ var REACTIVE_TEMPLATE_DIRECTIVE_RE = /@for|:for|@if|:if|@else|:else|x-for|x-if|x-else/;
8705
8706
  function buildDynamicRouteRegexes(fileRouteBase) {
8706
8707
  const toPattern = (p) => p.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
8707
8708
  const patterns = [new RegExp(`^${toPattern(fileRouteBase)}$`)];
@@ -9289,7 +9290,7 @@ After installing: bun add bun-plugin-stx`);
9289
9290
  pageMiddlewarePatterns.length = 0;
9290
9291
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9291
9292
  try {
9292
- const { Router } = await import("./chunk-0kwywfp1.js");
9293
+ const { Router } = await import("./chunk-atra2pxg.js");
9293
9294
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9294
9295
  const router = new Router(process19.cwd(), { pagesDirs, stateDir: stateDirName() });
9295
9296
  if (!options.quiet)
@@ -9518,8 +9519,8 @@ After installing: bun add bun-plugin-stx`);
9518
9519
  const dependencies = new Set;
9519
9520
  output = await processDirectives(output, context, filePath, config5, dependencies);
9520
9521
  output = await injectRouterScript(output, getRouterInjectOptions());
9521
- const directiveTemplateRe = /@for|:for|@if|:if|x-for|x-if/;
9522
- const hasDirectiveTemplates = /<template\s[^>]*(?:@for|:for|@if|:if|x-for|x-if)/.test(output);
9522
+ const directiveTemplateRe = REACTIVE_TEMPLATE_DIRECTIVE_RE;
9523
+ const hasDirectiveTemplates = new RegExp(`<template\\s[^>]*(?:${REACTIVE_TEMPLATE_DIRECTIVE_RE.source})`).test(output);
9523
9524
  if (hasDirectiveTemplates) {
9524
9525
  output = output.replace(/<template(?:\s[^>]*)?>|<\/template>/gi, (match) => {
9525
9526
  if (directiveTemplateRe.test(match))
@@ -9702,7 +9703,7 @@ ${output}`;
9702
9703
  }
9703
9704
  }
9704
9705
  output = await injectRouter(output, getRouterInjectOptions());
9705
- const directiveTemplateRe2 = /@for|:for|@if|:if|x-for|x-if/;
9706
+ const directiveTemplateRe2 = REACTIVE_TEMPLATE_DIRECTIVE_RE;
9706
9707
  output = output.replace(/<template(?:\s[^>]*)?>|<\/template>/gi, (match) => {
9707
9708
  if (directiveTemplateRe2.test(match))
9708
9709
  return match;
@@ -10103,6 +10104,7 @@ ${html}`;
10103
10104
  const groupMetaMatch = content.match(/<meta\b[^>]*name=["']stx-layout-group["'][^>]*content=["']([^"']+)["'][^>]*>/i);
10104
10105
  const pageLayoutGroup = i18nConfig && i18nLocale ? `i18n:${i18nLocale}` : groupMetaMatch?.[1] || deriveLayoutGroup(pageLayout);
10105
10106
  let fragment = content;
10107
+ let containerAttrs = "";
10106
10108
  const headStyles = [];
10107
10109
  const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
10108
10110
  if (headMatch) {
@@ -10139,6 +10141,7 @@ ${html}`;
10139
10141
  headStyles.push(bodyStyleMatch[0]);
10140
10142
  }
10141
10143
  }
10144
+ containerAttrs = mainOpenMatch[0].replace(/^<main\b/i, "").replace(/\/?>$/, "").trim();
10142
10145
  const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
10143
10146
  fragment = fragment.slice(mainStart, mainCloseIdx).trim();
10144
10147
  }
@@ -10174,6 +10177,7 @@ ${pageSetupScripts.join(`
10174
10177
  "X-STX-Layout": pageLayout,
10175
10178
  "X-STX-Layout-Group": pageLayoutGroup,
10176
10179
  ...pageTitle && { "X-STX-Title": encodeURIComponent(pageTitle) },
10180
+ ...containerAttrs && { "X-STX-Container-Attrs": encodeURIComponent(containerAttrs) },
10177
10181
  "Cache-Control": "no-store",
10178
10182
  ...corsHeaders
10179
10183
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.110",
4
+ "version": "0.2.112",
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",