stx-router 0.2.153 → 0.2.155

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 +211 -14
  2. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -253,11 +253,28 @@ export function getRouterScript() {
253
253
  // _cleanupContainer removed on the way out. Anything unstamped falls back to
254
254
  // the old sniff, so a page rendered by an older server behaves exactly as
255
255
  // before.
256
+ //
257
+ // The sniff itself now asks what the script REGISTERS rather than how it is
258
+ // spelled (#1828). cleanupContainer deletes window.stx._scopes entries,
259
+ // element disposers and destroy hooks on the way out, so the scripts that
260
+ // must run again are exactly the ones that put a scope back. That is
261
+ // detectable; a leading character is not.
262
+ //
263
+ // Deliberately NOT "strip leading comments, then test the first character",
264
+ // which is the obvious repair and is wrong in the expensive direction. Three
265
+ // emitters open with a comment and MUST NOT re-run: the animation scripts
266
+ // register matchMedia and DOMContentLoaded listeners plus an
267
+ // IntersectionObserver, and the STX lifecycle runtime owns a Map of live
268
+ // instances. Comment-stripping would flip all three to re-running on every
269
+ // navigation, leaking a listener and an observer each time and resetting the
270
+ // instance registry \u2014 trading a silent missing component for a silent leak.
271
+ var REGISTERS_SCOPE=/window\\.stx\\.mount|_scopes\\s*\\[|\\.initScope\\s*\\(|__stxComponentFactories\\s*\\[/;
256
272
  function runsAlways(declared, code){
257
273
  if(declared==='always')return true;
258
274
  if(declared==='once')return false;
275
+ if(REGISTERS_SCOPE.test(code))return true;
259
276
  var head=code.trimStart();
260
- return head.charAt(0)==='('||head.charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
277
+ return head.charAt(0)==='('||head.charAt(0)===';';
261
278
  }
262
279
  function hashScript(code){
263
280
  var h=0;for(var i=0;i<code.length;i++){h=((h<<5)-h)+code.charCodeAt(i);h|=0}
@@ -270,11 +287,99 @@ export function getRouterScript() {
270
287
  var match=code.match(/function (__stx_setup_[A-Za-z0-9_]+)\\s*\\(/);
271
288
  return match?match[1]:'';
272
289
  }
290
+ // Does this fetched DOCUMENT carry the signals runtime?
291
+ //
292
+ // Unlike a fragment, a document holds the answer directly, so there is
293
+ // nothing to infer from markers or ask the server for: look for the script.
294
+ // Both emitted shapes carry data-stx-runtime \u2014 inline, and the serve-mode
295
+ // src="/_stx/runtime.js" \u2014 with the content sniff behind it for a document
296
+ // rendered by a server old enough not to stamp the attribute.
297
+ function documentShipsRuntime(doc,html){
298
+ if(doc&&doc.querySelector&&doc.querySelector('script[data-stx-runtime]'))return true;
299
+ return html.indexOf("'use strict';var cloakStyle")!==-1
300
+ ||(html.indexOf('_cleanupContainer')!==-1&&html.indexOf('signals runtime loading')!==-1);
301
+ }
273
302
  function isSignalsRuntimeScript(script,code){
274
303
  return !!(script&&script.hasAttribute&&script.hasAttribute('data-stx-runtime'))
275
304
  ||(code.indexOf('_cleanupContainer')!==-1&&code.indexOf('signals runtime loading')!==-1)
276
305
  ||code.indexOf("'use strict';var cloakStyle")!==-1;
277
306
  }
307
+ // A fragment never carries the signals runtime: the server strips the runtime
308
+ // IIFE out of it, and every re-execution path here filters it out again. So a
309
+ // page that never needed a runtime cannot hydrate a fragment that does \u2014 the
310
+ // setup function would be defined but never invoked, scope IIFEs throw while
311
+ // destructuring window.stx, and every x-cloak element stays display:none
312
+ // forever against the cloak style shipped on EVERY page. Hand those off to a
313
+ // real navigation, which loads the destination's own runtime (#1809).
314
+ //
315
+ // The SERVER decides this now, and says so in X-STX-Runtime, which travels
316
+ // into the fragment marker as rt=1 / rt=0 so it survives the prefetch cache.
317
+ // It is answering a question it knows the answer to exactly \u2014 did I put a
318
+ // runtime in this page \u2014 where the router could only guess from markup.
319
+ //
320
+ // Guessing missed a whole page shape (#1827). Every marker below originates in
321
+ // a client SCRIPT, so a page with reactive syntax and no script anywhere \u2014 a
322
+ // <script server>-only login form built from :if / x-model / :disabled \u2014 emits
323
+ // none of them, while the server ships it the runtime. Whatever root marker it
324
+ // does get lands on <body>, which a fragment excludes by construction. So
325
+ // <p :if="error" x-cloak>Invalid credentials</p>
326
+ // swapped into a runtime-less page keeps the x-cloak the server stamped on it,
327
+ // and only the runtime ever removes that: server-rendered text, in the DOM, at
328
+ // display:none forever.
329
+ //
330
+ // The same held for x-data on the routed container, whose scope attribute is
331
+ // hoisted out of the fragment body into X-STX-Container-Attrs. Asking the
332
+ // server covers both without either being enumerated.
333
+ //
334
+ // The sniff stays as the fallback for a server too old to declare it. Its
335
+ // discriminators matter. x-cloak and data-stx-scoped are NOT usable: each
336
+ // fragment inlines the head styles, cloak rule included, and the appearance
337
+ // bootstrap emits a self-contained data-stx-scoped script on pages with no
338
+ // reactivity at all. A bare window.stx is not usable either \u2014 the server
339
+ // prepends a _latestSetup=null clear script to every fragment. Any of those
340
+ // would make this always true and disable SPA navigation everywhere.
341
+ function fragmentNeedsRuntime(frag,declared){
342
+ if(declared==='1')return true;
343
+ if(declared==='0')return false;
344
+ if(frag.indexOf('__stx_setup_')!==-1
345
+ ||frag.indexOf('data-stx-scope=')!==-1
346
+ ||frag.indexOf('data-stx-reactive')!==-1
347
+ ||/=\\s*window\\.stx\\s*;/.test(frag))return true;
348
+ var content=stripInertRegions(frag);
349
+ return BARE_DIRECTIVE.test(content)||content.indexOf('{{')!==-1;
350
+ }
351
+ // Documentation is both the largest population of runtime-less pages and the
352
+ // likeliest to contain directive syntax as literal CONTENT, so the fallback
353
+ // reads past the regions where that content lives. A page of stx examples must
354
+ // not full-reload on every hop through the docs \u2014 that is the regression
355
+ // #1809 existed to remove.
356
+ function stripInertRegions(frag){
357
+ return frag
358
+ .replace(new RegExp('<scr'+'ipt\\\\b[\\\\s\\\\S]*?<\\\\/scr'+'ipt>','gi'),'')
359
+ .replace(/<style\\b[\\s\\S]*?<\\/style>/gi,'')
360
+ .replace(/<pre\\b[\\s\\S]*?<\\/pre>/gi,'')
361
+ .replace(/<code\\b[\\s\\S]*?<\\/code>/gi,'')
362
+ .replace(/<!--[\\s\\S]*?-->/g,'');
363
+ }
364
+ // Anchored on the whitespace before an attribute name and the = that follows,
365
+ // so it reads attribute POSITIONS rather than characters. Bare ':' and '@' are
366
+ // everywhere in ordinary content \u2014 'xlink:href', 'background: red', '10:00',
367
+ // an email address \u2014 and none of those sit in that position.
368
+ //
369
+ // The x- arm is deliberately open rather than a list of known directives:
370
+ // x-attr is a GENERIC binding prefix, so x-href / x-src / x-value / x-alt bind
371
+ // exactly like x-text does. Measured over the 339 rendered docs pages, the
372
+ // open form costs nothing over an allowlist (3 matches either way, all three
373
+ // of them pages the CURRENT four-marker sniff already matches because they
374
+ // document its marker names) while covering directives an allowlist forgets.
375
+ var BARE_DIRECTIVE=/\\s(?::[a-z][\\w-]*|@[a-z][\\w.-]*|x-[a-z][\\w-]*)\\s*=\\s*["']/i;
376
+ // The fragment marker doubles as the carrier for the server's declaration.
377
+ // Encoding it in the HTML rather than threading a parameter keeps it inside
378
+ // the one value that already reaches swap() through every path, the prefetch
379
+ // cache included.
380
+ function fragmentMarker(declared){
381
+ return '<!--stx-fragment'+(declared==='true'?' rt=1':declared==='false'?' rt=0':'')+'-->';
382
+ }
278
383
  // Record scripts from the initial page load
279
384
  document.querySelectorAll('script').forEach(function(s){
280
385
  var text=s.textContent||'';
@@ -335,7 +440,20 @@ export function getRouterScript() {
335
440
  return containerSel==='main'||containerSel==='[data-stx-content]';
336
441
  }
337
442
 
443
+ // Write the address bar. The mode travels in the same slot as the legacy
444
+ // pushState boolean: false still means "do not touch history", true still
445
+ // pushes, and the string 'replace' replaces. Threading a fourth argument
446
+ // through swap() to each of the three history sites would have been the
447
+ // alternative (#1807).
448
+ function writeHistory(mode,href){
449
+ if(mode==='replace')history.replaceState({},'',href);
450
+ else history.pushState({},'',href);
451
+ }
452
+
453
+ // Second arg accepts the legacy pushState boolean OR an options object
454
+ // { replace }. Callers inside this file still pass the boolean.
338
455
  function navigate(url,pushState,force){
456
+ if(pushState&&typeof pushState==='object')pushState=pushState.replace?'replace':true;
339
457
  // Lang-picker passes force=true with an already-localized path (/en/...).
340
458
  // Re-localizing would map it back to the *current* locale and no-op.
341
459
  if(!force) url=withCurrentLocale(url);
@@ -351,7 +469,7 @@ export function getRouterScript() {
351
469
  if(t.origin!==location.origin){location.href=url;return Promise.resolve(false)}
352
470
 
353
471
  if(t.pathname===location.pathname&&t.hash){
354
- if(pushState!==false)history.pushState({},'',t.href);
472
+ if(pushState!==false)writeHistory(pushState,t.href);
355
473
  var el=document.querySelector(t.hash);
356
474
  if(el)el.scrollIntoView({behavior:'smooth'});
357
475
  return Promise.resolve(true);
@@ -387,6 +505,7 @@ export function getRouterScript() {
387
505
  return r.text();
388
506
  }).then(function(html){
389
507
  log('[router] full page fetched from cache path, len:',html.length);
508
+ pendingLayoutDecl={layout:layoutCache[targetPath]||'',group:layoutGroupCache[targetPath]||''};
390
509
  return swap(html,targetPath,pushState,targetHash);
391
510
  }).catch(function(err){
392
511
  console.error('[router] full page fetch error:',err);
@@ -394,6 +513,7 @@ export function getRouterScript() {
394
513
  }).finally(done);
395
514
  }
396
515
  pendingContainerAttrs=attrsCache[targetPath]||'';
516
+ pendingLayoutDecl=null;
397
517
  return Promise.resolve(swap(cache[targetPath],targetPath,pushState,targetHash)).then(function(){
398
518
  applyTitle(titleCache[targetPath]);
399
519
  return true;
@@ -416,6 +536,7 @@ else {
416
536
  var newGroup=r.headers.get('X-STX-Layout-Group')||'';
417
537
  var newTitle=r.headers.get('X-STX-Title')||'';
418
538
  var newCAttrs=r.headers.get('X-STX-Container-Attrs')||'';
539
+ var newRuntime=r.headers.get('X-STX-Runtime')||'';
419
540
  // Layout change? Fetch the FULL page (no X-STX-Router header) and do full document swap
420
541
  if(isFragment&&checkLayoutChange(newLayout,url,newGroup)){
421
542
  log('[router] layout change \u2014 fetching full page for document swap');
@@ -428,12 +549,13 @@ else {
428
549
  });
429
550
  });
430
551
  }
431
- return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle,containerAttrs:newCAttrs}});
552
+ return r.text().then(function(html){return{html:html,isFragment:isFragment,layout:newLayout,layoutGroup:newGroup,title:newTitle,containerAttrs:newCAttrs,runtime:newRuntime}});
432
553
  }).then(function(result){
433
554
  if(!result)return;
434
- if(result.isFragment)result.html='<!--stx-fragment-->'+result.html;
555
+ if(result.isFragment)result.html=fragmentMarker(result.runtime)+result.html;
435
556
  if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup,result.title,result.containerAttrs);
436
557
  pendingContainerAttrs=result.isFragment?(result.containerAttrs||''):'';
558
+ pendingLayoutDecl=result.isFragment?null:{layout:result.layout||'',group:result.layoutGroup||''};
437
559
  return Promise.resolve(swap(result.html,targetPath,pushState,targetHash)).then(function(){
438
560
  // Fragment swaps carry no <head>; apply the title from the header.
439
561
  // Full-document swaps already set document.title from the <title> tag.
@@ -458,9 +580,19 @@ else {
458
580
  ||html.indexOf('data-stx-content')!==-1;
459
581
  }
460
582
 
583
+ // The layout and group the SERVER declared for the document about to be
584
+ // swapped in. Set immediately before each swap() call by whichever path
585
+ // fetched it, and consumed once.
586
+ //
587
+ // Threaded this way rather than as a swap() parameter, matching
588
+ // pendingContainerAttrs \u2014 see the note above writeHistory about the cost of
589
+ // adding an argument to swap() and its three call sites (#1807).
590
+ var pendingLayoutDecl=null;
461
591
  function swap(html,url,pushState,hash){
462
- var isFragment=html.indexOf('<!--stx-fragment-->')===0;
463
- if(isFragment)html=html.slice('<!--stx-fragment-->'.length);
592
+ var fragMark=/^<!--stx-fragment(?: rt=([01]))?-->/.exec(html);
593
+ var isFragment=!!fragMark;
594
+ var declaredRuntime=fragMark&&fragMark[1]?fragMark[1]:'';
595
+ if(isFragment)html=html.slice(fragMark[0].length);
464
596
  if(!isFragment&&!isStxDocument(html)){log('[router] non-stx document \u2014 full navigation to:',url);location.href=url;return Promise.resolve(false)}
465
597
  var currentContent=getContainer();
466
598
  log('[router] swap: isFragment='+isFragment+' container='+!!currentContent+' tag='+(currentContent&&currentContent.tagName)+' selector='+containerSel+' htmlLen='+html.length);
@@ -468,6 +600,14 @@ else {
468
600
 
469
601
  // Fragment mode: server returned just the page content (no document wrapper)
470
602
  if(isFragment){
603
+ // Before _cleanupContainer, not after: handing off later would dispose
604
+ // the OUTGOING page's scopes and then reload anyway. Inside doFragSwap
605
+ // would be later still \u2014 innerHTML is already replaced by then.
606
+ if(!window.stx&&fragmentNeedsRuntime(html,declaredRuntime)){
607
+ log('[router] fragment needs the signals runtime and this page has none \u2014 full navigation to:',url);
608
+ location.href=url;
609
+ return Promise.resolve(false);
610
+ }
471
611
  if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
472
612
  function doFragSwap(){
473
613
  // Extract scripts from fragment before injecting HTML
@@ -477,6 +617,22 @@ else {
477
617
  var fragCrosswindHrefs=[];
478
618
  var fragCrosswindCSS=null;
479
619
  var cleanFrag=html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/scr'+'ipt>','gi'),function(m,attrs,code){
620
+ // A data block is not code, and everything below this line assumes it
621
+ // is: the body gets stashed in fragScripts, re-emitted as a pending
622
+ // placeholder, then wrapped in braces and executed. Wrapping a JSON
623
+ // object in braces reparses it as a labelled block, so its first ':'
624
+ // is a SyntaxError on every single fragment swap
625
+ // (stacksjs/stx#1801). @structuredData renders at its call site,
626
+ // which inside a layout is @section('content') \u2014 i.e. in the routed
627
+ // container \u2014 so the natural way to write it hit this.
628
+ //
629
+ // Left exactly as found rather than stripped: crawlers read
630
+ // structured data wherever it sits, and the browser will not execute
631
+ // a non-JS type anyway. Mirrors prepareRoutedBodyScripts' notion of
632
+ // executable, which this path never had.
633
+ var typeMatch=attrs.match(/\\btype\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i);
634
+ var scriptType=((typeMatch&&(typeMatch[1]||typeMatch[2]||typeMatch[3]))||'').trim().toLowerCase();
635
+ if(scriptType&&scriptType!=='text/javascript'&&scriptType!=='application/javascript'&&scriptType!=='module')return m;
480
636
  if(code&&code.trim()&&!isSignalsRuntimeScript({hasAttribute:function(name){return name==='data-stx-runtime'&&attrs.indexOf('data-stx-runtime')!==-1}},code)){
481
637
  var slot='fragment-'+(++fragScriptId);
482
638
  var scoped=/(?:^|\\s)data-stx-scoped(?:\\s|=|$)/i.test(attrs);
@@ -548,7 +704,7 @@ else {
548
704
  currentContent.innerHTML=cleanFrag;
549
705
  // Remove old page scripts
550
706
  document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
551
- if(pushState!==false)history.pushState({},'',url+(hash||''));
707
+ if(pushState!==false)writeHistory(pushState,url+(hash||''));
552
708
  updateNav(url);
553
709
  updateActiveLinks();
554
710
  if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'});
@@ -618,7 +774,12 @@ else {
618
774
  document.body.appendChild(ns);
619
775
  }
620
776
  });
621
- log('[router] scripts done. _latestSetup:', !!window.stx._latestSetup);
777
+ // Guarded: the router ships on every page, the signals runtime only on
778
+ // pages that have a client script. log() is debug-gated but its
779
+ // ARGUMENTS are evaluated at the call site regardless, so an unguarded
780
+ // read here threw on every runtime-less page \u2014 aborting the swap and
781
+ // falling back to a full document load (#1809).
782
+ log('[router] scripts done. _latestSetup:', !!(window.stx&&window.stx._latestSetup));
622
783
  // THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
623
784
  window.dispatchEvent(new Event('stx:load'));
624
785
  }
@@ -642,6 +803,22 @@ else {
642
803
  var docBuildMeta=doc.querySelector('meta[name="stx-build"]');
643
804
  var docBuild=docBuildMeta?(docBuildMeta.getAttribute('content')||''):'';
644
805
  if(isBuildSkew(docBuild)){reloadForSkew(url,docBuild);return Promise.resolve(false)}
806
+ // Same hand-off as the fragment path, for the same reason (#1809, #1839).
807
+ // A full document DOES carry the destination's runtime \u2014 and this path
808
+ // throws it away: prepareRoutedBodyScripts drops every script[src] and
809
+ // everything isSignalsRuntimeScript matches, on the assumption that the
810
+ // current page already has one. When it does not, nothing puts a runtime
811
+ // back, so the destination's client code dies on its first line with
812
+ // "defineStore is not defined" (defineStore lives on window.stx) and the
813
+ // page renders quietly inert \u2014 no error reaches the user.
814
+ //
815
+ // Before the swap and before _cleanupContainer, so we neither dispose the
816
+ // outgoing page's scopes nor half-replace the document and then reload.
817
+ if(!window.stx&&documentShipsRuntime(doc,html)){
818
+ log('[router] document needs the signals runtime and this page has none \u2014 full navigation to:',url);
819
+ location.href=url;
820
+ return Promise.resolve(false);
821
+ }
645
822
  var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
646
823
  if(!newContent){location.href=url;return Promise.resolve(false)}
647
824
 
@@ -728,14 +905,33 @@ else {
728
905
  // For same-layout: swap only the container (<main>)
729
906
  var newBody=doc.querySelector('body');
730
907
  var isLayoutChange=false;
908
+ var declaredLayout=pendingLayoutDecl;
909
+ pendingLayoutDecl=null;
731
910
  if(newBody){
732
911
  var newMeta=doc.querySelector('meta[name="stx-layout"]');
733
912
  var curMeta=document.querySelector('meta[name="stx-layout"]');
734
913
  var curLayout=curMeta?curMeta.getAttribute('content'):'';
735
- var newLayout=newMeta?newMeta.getAttribute('content'):'';
914
+ // The server's answer wins over the document's metas. We are only here
915
+ // because checkLayoutChange already said the group changed, based on
916
+ // exactly these headers \u2014 re-deriving from metas and disagreeing meant
917
+ // paying for the full-document fetch and then discarding the reason we
918
+ // made it, so the destination's <main> was swapped into the previous
919
+ // page's chrome (#1833).
920
+ //
921
+ // They disagree more easily than they look. The bun-plugin serve path
922
+ // calls a layout-less page 'default' while the client's own fallback
923
+ // calls it 'app'; a <head> with attributes silently skips the meta
924
+ // injection entirely; and the i18n path rewrites every page's group to
925
+ // i18n:<locale>. Any one of those leaves the metas saying "unchanged"
926
+ // about a document the headers said had changed.
927
+ var newLayout=declaredLayout&&declaredLayout.layout
928
+ ? declaredLayout.layout
929
+ : (newMeta?newMeta.getAttribute('content'):'');
736
930
  var curGroup=getCurrentLayoutGroup();
737
- var newGroup=getDocLayoutGroup(doc,newLayout);
738
- isLayoutChange=curGroup!==newGroup||(curLayout&&newLayout&&curLayout!==newLayout);
931
+ var newGroup=declaredLayout&&declaredLayout.group
932
+ ? declaredLayout.group
933
+ : getDocLayoutGroup(doc,newLayout);
934
+ isLayoutChange=curGroup!==newGroup||!!(curLayout&&newLayout&&curLayout!==newLayout);
739
935
  }
740
936
  var incomingSetupName=newBody?(newBody.getAttribute('data-stx')||''):'';
741
937
  // Scripts assigned through innerHTML are inert. Preserve executable
@@ -862,7 +1058,7 @@ else {
862
1058
  }
863
1059
 
864
1060
  // Push history state (before active link updates so location.pathname is current)
865
- if(pushState!==false)history.pushState({},'',url+(hash||''));
1061
+ if(pushState!==false)writeHistory(pushState,url+(hash||''));
866
1062
 
867
1063
  // Update active nav links
868
1064
  updateNav(url);
@@ -1044,7 +1240,8 @@ else {
1044
1240
  var pLayout=r.headers.get('X-STX-Layout')||'';
1045
1241
  var pGroup=r.headers.get('X-STX-Layout-Group')||'';
1046
1242
  var pTitle=r.headers.get('X-STX-Title')||'';
1047
- return r.text().then(function(html){return{html:isFrag?'<!--stx-fragment-->'+html:html,layout:pLayout,layoutGroup:pGroup,title:pTitle}});
1243
+ var pRuntime=r.headers.get('X-STX-Runtime')||'';
1244
+ return r.text().then(function(html){return{html:isFrag?fragmentMarker(pRuntime)+html:html,layout:pLayout,layoutGroup:pGroup,title:pTitle}});
1048
1245
  }).then(function(result){
1049
1246
  if(o.cache)setCache(key,result.html,result.layout,result.layoutGroup,result.title);
1050
1247
  }).catch(function(){}).finally(function(){delete prefetching[key]});
@@ -1143,7 +1340,7 @@ else {
1143
1340
  var key=cacheKey(url);
1144
1341
  if(!cache[key]){
1145
1342
  var wantsFragment=shouldUseFragmentResponse();
1146
- 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')||'';var pTitle=r.headers.get('X-STX-Title')||'';return r.text().then(function(html){return{html:isFrag?'<!--stx-fragment-->'+html:html,layout:pLayout,layoutGroup:pGroup,title:pTitle}})}).then(function(result){setCache(key,result.html,result.layout,result.layoutGroup,result.title)}).catch(function(){});
1343
+ 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')||'';var pTitle=r.headers.get('X-STX-Title')||'';var pRuntime=r.headers.get('X-STX-Runtime')||'';return r.text().then(function(html){return{html:isFrag?fragmentMarker(pRuntime)+html:html,layout:pLayout,layoutGroup:pGroup,title:pTitle}})}).then(function(result){setCache(key,result.html,result.layout,result.layoutGroup,result.title)}).catch(function(){});
1147
1344
  }
1148
1345
  },
1149
1346
  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];cacheOrder.length=0},
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stx-router",
3
3
  "type": "module",
4
- "version": "0.2.153",
4
+ "version": "0.2.155",
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",