bun-plugin-stx 0.2.291 → 0.2.292

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.
@@ -647,7 +647,7 @@ function getRouterScript() {
647
647
  if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
648
648
 
649
649
  // \u2500\u2500 Configuration \u2500\u2500
650
- var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false};
650
+ var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false,prefetchCacheMax:50,routeFocus:true,announceRoute:true,interceptForms:false,cssLoadTimeout:1500};
651
651
  var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
652
652
  var containerSel=o.container;
653
653
  var debug=!!o.debug;
@@ -1411,6 +1411,99 @@ else {
1411
1411
  try{target.focus({preventScroll:true})}catch(e){try{target.focus()}catch(e2){}}
1412
1412
  }
1413
1413
 
1414
+ // \u2500\u2500 Generated utility stylesheets \u2500\u2500
1415
+ // Every page ships its own content-addressed utility sheet as
1416
+ // <link data-css="generated">, and none of them is ever removed on a
1417
+ // navigation, so they pile up in <head>. Two of them can define the same
1418
+ // class at the same specificity, which makes their ORDER decide the cascade:
1419
+ // the later sheet wins. Deduping alone left that order as "first seen", so
1420
+ // after Records \u2192 Feed \u2192 Records the Feed sheet still sat after the Records
1421
+ // one and its .flex-col beat Records' own responsive sm:flex-row \u2014 the header
1422
+ // stacked into a column on desktop. The current page's complete sheet has to
1423
+ // be the last one, every navigation, not just the first time it is seen.
1424
+ function findGeneratedCss(abs){
1425
+ var found=null;
1426
+ document.querySelectorAll('head link[data-css][href]').forEach(function(link){
1427
+ if(new URL(link.getAttribute('href'),location.href).href!==abs)return;
1428
+ // Collapse duplicates, so moving one copy to the end can never leave an
1429
+ // older copy of the same sheet behind to fight it.
1430
+ if(found){link.parentNode.removeChild(link);return}
1431
+ found=link;
1432
+ });
1433
+ return found;
1434
+ }
1435
+
1436
+ // Before the swap: add each sheet the page does not have yet, as
1437
+ // media="print" so it downloads without restyling the page still on screen.
1438
+ // Returns a promise for them to finish (load OR error, capped at
1439
+ // cssLoadTimeout so a slow or missing sheet never stalls navigation), or null
1440
+ // when nothing new is needed and the swap can go ahead in the same turn.
1441
+ function preloadGeneratedCss(hrefs,base){
1442
+ var pending=[];
1443
+ hrefs.forEach(function(href){
1444
+ var abs;
1445
+ try{abs=new URL(href,base).href}catch(e){return}
1446
+ var link=findGeneratedCss(abs);
1447
+ if(link){
1448
+ // Still downloading for a navigation that never swapped \u2014 wait on it
1449
+ // rather than on nothing.
1450
+ if(link.__stxCssReady)pending.push(link.__stxCssReady);
1451
+ return;
1452
+ }
1453
+ link=document.createElement('link');
1454
+ link.setAttribute('data-css','generated');
1455
+ link.setAttribute('rel','stylesheet');
1456
+ link.setAttribute('href',new URL(href,location.href).href===abs?href:abs);
1457
+ link.setAttribute('media','print');
1458
+ link.setAttribute('data-stx-css-pending','');
1459
+ link.__stxCssReady=new Promise(function(resolve){
1460
+ var timer=setTimeout(finish,o.cssLoadTimeout);
1461
+ function finish(){clearTimeout(timer);link.onload=null;link.onerror=null;link.__stxCssReady=null;resolve()}
1462
+ link.onload=finish;
1463
+ link.onerror=finish;
1464
+ });
1465
+ pending.push(link.__stxCssReady);
1466
+ document.head.appendChild(link);
1467
+ });
1468
+ return pending.length?Promise.all(pending):null;
1469
+ }
1470
+
1471
+ // At the swap: make the destination's sheets live and move them to the end
1472
+ // of <head>. appendChild on a node that is already there MOVES it \u2014 no
1473
+ // duplicate, and the browser keeps the parsed sheet rather than refetching.
1474
+ // Only generated sheets are reordered; app and font stylesheets keep the
1475
+ // order they were declared in.
1476
+ function promoteGeneratedCss(hrefs,base){
1477
+ hrefs.forEach(function(href){
1478
+ var abs;
1479
+ try{abs=new URL(href,base).href}catch(e){return}
1480
+ var link=findGeneratedCss(abs);
1481
+ if(!link){
1482
+ preloadGeneratedCss([href],base);
1483
+ link=findGeneratedCss(abs);
1484
+ if(!link)return;
1485
+ }
1486
+ if(link.hasAttribute('data-stx-css-pending')){
1487
+ link.removeAttribute('data-stx-css-pending');
1488
+ link.removeAttribute('media');
1489
+ }
1490
+ document.head.appendChild(link);
1491
+ });
1492
+ }
1493
+
1494
+ // The generated-sheet hrefs a fragment carries. Scripts are skipped so a
1495
+ // <link> written inside a string in page code is not mistaken for one.
1496
+ function fragmentCssHrefs(html){
1497
+ var hrefs=[];
1498
+ html.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt>','gi'),'').replace(new RegExp('<link\\\\b([^>]*)>','gi'),function(m,attrs){
1499
+ if(attrs.indexOf('data-css')===-1)return m;
1500
+ var hrefMatch=attrs.match(/\\bhref=(["'])(.*?)\\1/i);
1501
+ if(hrefMatch&&hrefMatch[2])hrefs.push(hrefMatch[2]);
1502
+ return m;
1503
+ });
1504
+ return hrefs;
1505
+ }
1506
+
1414
1507
  function swap(html,url,pushState,hash){
1415
1508
  var fragMark=/^<!--stx-fragment(?: rt=([01]))?-->/.exec(html);
1416
1509
  var isFragment=!!fragMark;
@@ -1431,7 +1524,6 @@ else {
1431
1524
  location.href=url;
1432
1525
  return Promise.resolve(false);
1433
1526
  }
1434
- if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
1435
1527
  function doFragSwap(){
1436
1528
  // Extract scripts from fragment before injecting HTML
1437
1529
  var fragScripts=[];
@@ -1508,19 +1600,9 @@ else {
1508
1600
  document.head.appendChild(cw);
1509
1601
  }
1510
1602
  }
1511
- fragCssHrefs.forEach(function(href){
1512
- var absolute=new URL(href,location.href).href;
1513
- var exists=false;
1514
- document.querySelectorAll('head link[data-css][href]').forEach(function(link){
1515
- if(new URL(link.getAttribute('href'),location.href).href===absolute)exists=true;
1516
- });
1517
- if(exists)return;
1518
- var link=document.createElement('link');
1519
- link.setAttribute('data-css','generated');
1520
- link.setAttribute('rel','stylesheet');
1521
- link.setAttribute('href',href);
1522
- document.head.appendChild(link);
1523
- });
1603
+ // Already present ones are MOVED last, not skipped \u2014 see
1604
+ // promoteGeneratedCss for why their order is the whole bug.
1605
+ promoteGeneratedCss(fragCssHrefs,location.href);
1524
1606
  // Add new page styles
1525
1607
  fragStyles.forEach(function(s){
1526
1608
  var el=document.createElement('style');
@@ -1639,16 +1721,25 @@ else {
1639
1721
  if(fragPending.length)Promise.all(fragPending).then(runFragScripts).catch(runFragScripts);
1640
1722
  else runFragScripts();
1641
1723
  }
1642
- return new Promise(function(resolve,reject){
1643
- function completeFragSwap(){
1644
- try{
1645
- doFragSwap();
1646
- resolve(true);
1647
- }catch(err){reject(err)}
1648
- }
1649
- if(runViewTransition(completeFragSwap)){}
1650
- else{currentContent.style.transition='opacity 0.12s ease-out';currentContent.style.opacity='0';setTimeout(function(){completeFragSwap();currentContent.style.opacity='1';setTimeout(function(){currentContent.style.transition=''},150)},120)}
1651
- });
1724
+ function startFragSwap(){
1725
+ if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
1726
+ return new Promise(function(resolve,reject){
1727
+ function completeFragSwap(){
1728
+ try{
1729
+ doFragSwap();
1730
+ resolve(true);
1731
+ }catch(err){reject(err)}
1732
+ }
1733
+ if(runViewTransition(completeFragSwap)){}
1734
+ else{currentContent.style.transition='opacity 0.12s ease-out';currentContent.style.opacity='0';setTimeout(function(){completeFragSwap();currentContent.style.opacity='1';setTimeout(function(){currentContent.style.transition=''},150)},120)}
1735
+ });
1736
+ }
1737
+ // A stylesheet this page has never loaded is fetched BEFORE the content
1738
+ // swaps, or the new markup paints once without its utilities and then
1739
+ // jumps. Cleanup waits with it so the outgoing page stays live meanwhile.
1740
+ // With nothing new to load this runs in the same turn, as it always did.
1741
+ var fragCssReady=preloadGeneratedCss(fragmentCssHrefs(html),location.href);
1742
+ return fragCssReady?fragCssReady.then(startFragSwap):startFragSwap();
1652
1743
  }
1653
1744
 
1654
1745
  // Full document mode: parse with DOMParser and extract container content
@@ -1678,10 +1769,13 @@ else {
1678
1769
  var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
1679
1770
  if(!newContent){location.href=url;return Promise.resolve(false)}
1680
1771
 
1681
- // Clean up existing signals/effects
1682
- if(window.stx&&window.stx._cleanupContainer){
1683
- window.stx._cleanupContainer(currentContent);
1684
- }
1772
+ // The destination's generated utility sheets, resolved against the page
1773
+ // being navigated to. Loaded before the swap and moved last during it \u2014
1774
+ // same reasons as the fragment path (promoteGeneratedCss).
1775
+ var docCssBase;
1776
+ try{docCssBase=new URL(url,location.href).href}catch(e){docCssBase=location.href}
1777
+ var docCssHrefs=[];
1778
+ doc.querySelectorAll('head link[data-css][href]').forEach(function(l){docCssHrefs.push(l.getAttribute('href'))});
1685
1779
 
1686
1780
  function doSwap(){
1687
1781
  // \u2500\u2500 Swap <head> styles \u2500\u2500
@@ -1748,6 +1842,9 @@ else {
1748
1842
  doc.querySelectorAll('head '+linkSel).forEach(function(l){
1749
1843
  var href=l.getAttribute('href');
1750
1844
  if(!href)return;
1845
+ // Generated utility sheets are not additive-only: promoteGeneratedCss
1846
+ // below also reorders them.
1847
+ if(l.hasAttribute('data-css'))return;
1751
1848
  var abs=new URL(href,docBase).href;
1752
1849
  if(curLinks[abs])return;
1753
1850
  curLinks[abs]=1;
@@ -1755,6 +1852,9 @@ else {
1755
1852
  Array.from(l.attributes).forEach(function(a){nl.setAttribute(a.name,a.value)});
1756
1853
  document.head.appendChild(nl);
1757
1854
  });
1855
+ // After every other stylesheet link, so the destination's complete
1856
+ // utility sheet is last in the cascade.
1857
+ promoteGeneratedCss(docCssHrefs,docBase);
1758
1858
 
1759
1859
  // \u2500\u2500 Swap content \u2500\u2500
1760
1860
  // For layout changes: swap the entire <body> to replace layout chrome (nav, footer, etc.)
@@ -2048,26 +2148,34 @@ else {
2048
2148
  }
2049
2149
  }
2050
2150
 
2051
- return new Promise(function(resolve,reject){
2052
- function completeSwap(){
2053
- try{
2054
- doSwap();
2055
- resolve(true);
2056
- }catch(err){reject(err)}
2057
- }
2058
- if(runViewTransition(completeSwap)){
2151
+ function startSwap(){
2152
+ // Clean up existing signals/effects
2153
+ if(window.stx&&window.stx._cleanupContainer){
2154
+ window.stx._cleanupContainer(currentContent);
2059
2155
  }
2060
- else {
2061
- // Fallback fade for browsers without View Transitions API
2062
- currentContent.style.transition='opacity 0.12s ease-out';
2063
- currentContent.style.opacity='0';
2064
- setTimeout(function(){
2065
- completeSwap();
2066
- currentContent.style.opacity='1';
2067
- setTimeout(function(){currentContent.style.transition=''},150);
2068
- },120);
2069
- }
2070
- });
2156
+ return new Promise(function(resolve,reject){
2157
+ function completeSwap(){
2158
+ try{
2159
+ doSwap();
2160
+ resolve(true);
2161
+ }catch(err){reject(err)}
2162
+ }
2163
+ if(runViewTransition(completeSwap)){
2164
+ }
2165
+ else {
2166
+ // Fallback fade for browsers without View Transitions API
2167
+ currentContent.style.transition='opacity 0.12s ease-out';
2168
+ currentContent.style.opacity='0';
2169
+ setTimeout(function(){
2170
+ completeSwap();
2171
+ currentContent.style.opacity='1';
2172
+ setTimeout(function(){currentContent.style.transition=''},150);
2173
+ },120);
2174
+ }
2175
+ });
2176
+ }
2177
+ var docCssReady=preloadGeneratedCss(docCssHrefs,docCssBase);
2178
+ return docCssReady?docCssReady.then(startSwap):startSwap();
2071
2179
  }
2072
2180
 
2073
2181
  // \u2500\u2500 Link interception \u2500\u2500
package/dist/serve.js CHANGED
@@ -9700,7 +9700,7 @@ function __stxOverlay(errs){
9700
9700
  pageMiddlewarePatterns.length = 0;
9701
9701
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9702
9702
  try {
9703
- const { Router } = await import("./chunk-hzb7bk1k.js");
9703
+ const { Router } = await import("./chunk-nb1x6zdk.js");
9704
9704
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9705
9705
  const router = new Router(process19.cwd(), { pagesDirs, stateDir: stateDirName() });
9706
9706
  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.291",
4
+ "version": "0.2.292",
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",