stx-router 0.2.337 → 0.2.339

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.
package/dist/client.d.ts CHANGED
@@ -1 +1,16 @@
1
+ /**
2
+ * The router script as it ships: debug logging removed, then minified.
3
+ *
4
+ * Every page carries this, so the 32 `log()` call sites -- 2.3KB of the
5
+ * delivered bytes, printing nothing unless `debug` is set -- are not part of
6
+ * it. Stripping happens outside the try, as in generateSignalsRuntime: losing
7
+ * minification makes the script bigger, losing the strip would ship the logs.
8
+ */
1
9
  export declare function getRouterScript(): string;
10
+ /**
11
+ * The router script with its `log()` calls intact, for development.
12
+ *
13
+ * Set `debug` in the router config to see them. Minified the same way, so what
14
+ * a developer runs behaves exactly like what ships.
15
+ */
16
+ export declare function getRouterScriptDev(): string;
package/dist/client.js CHANGED
@@ -1,4 +1,45 @@
1
- let cachedRouterScript;
1
+ let cachedRouterScript, cachedRouterScriptDev;
2
+ function stripRouterLogs(source) {
3
+ const out = [], needle = "log(";
4
+ let i = 0;
5
+ while (i < source.length) {
6
+ const hit = source.indexOf(needle, i);
7
+ if (hit === -1) {
8
+ out.push(source.slice(i));
9
+ break;
10
+ }
11
+ const before = hit > 0 ? source[hit - 1] : " ", isDeclaration = /\bfunction\s+$/.test(source.slice(Math.max(0, hit - 16), hit));
12
+ if (/[\w$.]/.test(before) || isDeclaration) {
13
+ out.push(source.slice(i, hit + needle.length));
14
+ i = hit + needle.length;
15
+ continue;
16
+ }
17
+ out.push(source.slice(i, hit));
18
+ let depth = 1, j = hit + needle.length;
19
+ while (j < source.length && depth > 0) {
20
+ const character = source[j];
21
+ if (character === '"' || character === "'" || character === "`") {
22
+ const quote = character;
23
+ j++;
24
+ while (j < source.length && source[j] !== quote) {
25
+ if (source[j] === "\\")
26
+ j++;
27
+ j++;
28
+ }
29
+ j++;
30
+ continue;
31
+ }
32
+ if (character === "(")
33
+ depth++;
34
+ else if (character === ")")
35
+ depth--;
36
+ j++;
37
+ }
38
+ out.push("0");
39
+ i = j;
40
+ }
41
+ return out.join("");
42
+ }
2
43
  function minifyRouterScript(source) {
3
44
  try {
4
45
  return new Bun.Transpiler({ loader: "js", minifyWhitespace: !0 }).transformSync(source).replace(/\}(var |let |const |function )/g, "};$1").trim();
@@ -6,22 +47,109 @@ function minifyRouterScript(source) {
6
47
  return source;
7
48
  }
8
49
  }
9
- export function getRouterScript() {
10
- if (cachedRouterScript)
11
- return cachedRouterScript;
12
- cachedRouterScript = minifyRouterScript(`
50
+ function routerSource() {
51
+ return `
13
52
  ;(function(){
14
53
  'use strict';
15
54
  var ROUTER_REV=7;
16
55
  if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
17
56
 
18
57
  // \u2500\u2500 Configuration \u2500\u2500
19
- 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};
58
+ 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,scrollRestoration:true};
20
59
  var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
21
60
  var containerSel=o.container;
22
61
  var debug=!!o.debug;
23
62
  function log(){if(debug&&typeof console!=='undefined'&&console.log)console.log.apply(console,arguments)}
24
63
 
64
+ // Shorthands for the document-rooted queries this script makes ~33 times.
65
+ // Every page carries these bytes, and the budget in
66
+ // scripts/performance-budgets.ts is what noticed.
67
+ var LAYOUT_META='meta[name="stx-layout"]';
68
+ var LAYOUT_GROUP_META='meta[name="stx-layout-group"]';
69
+ var BUILD_META='meta[name="stx-build"]';
70
+ function qs(sel){return document.querySelector(sel)}
71
+ function qsa(sel){return document.querySelectorAll(sel)}
72
+ function ce(tag){return document.createElement(tag)}
73
+ function dhead(){return document.head}
74
+
75
+ // \u2500\u2500 Scroll position \u2500\u2500
76
+ // Forward navigation goes to the top, or to the hash. Back and forward return
77
+ // to where the entry was left -- the browser cannot do that for us, because
78
+ // by the time it would restore, the content of the entry has not been fetched
79
+ // yet, so it clamps to whatever height the outgoing page happened to have.
80
+ // history.scrollRestoration therefore goes to manual and the positions are
81
+ // kept here, keyed by a token written into each history entry (two entries
82
+ // for the same URL keep their own positions, which is the point).
83
+ //
84
+ // Manual restoration also takes the browser's reload behaviour away, so the
85
+ // positions are mirrored into sessionStorage: a reload, or a return to the
86
+ // tab, finds the entry's token already in history.state and restores from
87
+ // there. sessionStorage is per tab and dies with it, same as the history.
88
+ var SCROLL_TOKEN='__stxScroll';
89
+ var SCROLL_STORE='stx:scroll:';
90
+ var scrollSeq=0;
91
+ var pendingScroll=null;
92
+ var restoreScroll=!!o.scrollRestoration;
93
+ function newScrollToken(){return 's'+(++scrollSeq)+'-'+Date.now()}
94
+ var scrollToken=(history.state&&history.state[SCROLL_TOKEN])||newScrollToken();
95
+ // sessionStorage is the only store: it is synchronous, it is per tab, it dies
96
+ // with the tab exactly as the history does, and it is the one that survives a
97
+ // reload. A private window that refuses it degrades to scrolling to the top.
98
+ function readScroll(token){
99
+ try{
100
+ var parts=(window.sessionStorage.getItem(SCROLL_STORE+token)||'').split(',');
101
+ return parts.length===2?[parseFloat(parts[0])||0,parseFloat(parts[1])||0]:null;
102
+ }catch(e){return null}
103
+ }
104
+ // Called while the outgoing entry is still the current one, so the position
105
+ // read here belongs to the token being written.
106
+ function rememberScroll(){
107
+ if(!restoreScroll)return;
108
+ try{window.sessionStorage.setItem(SCROLL_STORE+scrollToken,(window.pageXOffset||window.scrollX||0)+','+(window.pageYOffset||window.scrollY||0))}catch(e){}
109
+ }
110
+ // One place decides where a swap leaves the viewport, so the fragment path
111
+ // and the whole-document path cannot drift apart.
112
+ function applyScroll(hash){
113
+ if(pendingScroll){
114
+ var at=pendingScroll;
115
+ pendingScroll=null;
116
+ window.scrollTo({left:at[0],top:at[1],behavior:'instant'});
117
+ return;
118
+ }
119
+ if(o.scrollToTop&&!hash){window.scrollTo({top:0,behavior:'instant'});return}
120
+ if(hash){var el=qs(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
121
+ }
122
+ // Keeps whatever another script put in the entry; only adds the token.
123
+ function stampScrollToken(){
124
+ var state={};
125
+ var current=history.state;
126
+ if(current&&typeof current==='object')for(var key in current)state[key]=current[key];
127
+ state[SCROLL_TOKEN]=scrollToken;
128
+ try{history.replaceState(state,'',location.href)}catch(e){}
129
+ }
130
+ if(restoreScroll){
131
+ try{history.scrollRestoration='manual'}catch(e){}
132
+ // A token already in the entry means this document is a reload or a
133
+ // restore of an entry visited before, so its position is worth asking for.
134
+ var known=history.state&&history.state[SCROLL_TOKEN];
135
+ if(known){
136
+ var saved=readScroll(scrollToken);
137
+ if(saved&&(saved[0]||saved[1])){
138
+ var restoreAt=function(){window.scrollTo({left:saved[0],top:saved[1],behavior:'instant'})};
139
+ if(document.readyState==='complete')restoreAt();
140
+ else window.addEventListener('load',restoreAt,{once:true});
141
+ }
142
+ }
143
+ else{
144
+ stampScrollToken();
145
+ }
146
+ // A reload or a link out of the app never calls writeHistory, so this is
147
+ // the only chance to record where the page was left. pagehide fires for
148
+ // both, and unlike beforeunload it does not keep the page out of the
149
+ // back/forward cache.
150
+ window.addEventListener('pagehide',rememberScroll);
151
+ }
152
+
25
153
  // Replace, never merge: absent payloads must forget the outgoing page's data.
26
154
  // Run at the committed swap, not during prefetch or a superseded navigation.
27
155
  function hydrateServerData(html){
@@ -44,7 +172,7 @@ export function getRouterScript() {
44
172
  // Conservative on purpose: act only when BOTH ids are known. A missing id is
45
173
  // "no information", never "mismatch", so statically hosted output (no headers)
46
174
  // and older servers behave exactly as before.
47
- var buildMeta=document.querySelector('meta[name="stx-build"]');
175
+ var buildMeta=qs(BUILD_META);
48
176
  var loadedBuild=buildMeta?(buildMeta.getAttribute('content')||''):'';
49
177
  window.__stxBuild=loadedBuild;
50
178
  function isBuildSkew(incoming){
@@ -192,7 +320,7 @@ export function getRouterScript() {
192
320
  function parseContainerAttrs(attrStr){
193
321
  var map={};
194
322
  if(!attrStr)return map;
195
- var probe=document.createElement('div');
323
+ var probe=ce('div');
196
324
  probe.innerHTML='<i '+attrStr+'></i>';
197
325
  var src=probe.firstChild;
198
326
  // getAttributeNames first: it is the same standard DOM answer and it is
@@ -281,7 +409,7 @@ export function getRouterScript() {
281
409
  }
282
410
 
283
411
  function getContainer(){
284
- return document.querySelector(containerSel)||document.querySelector('[data-stx-content]')||document.querySelector('main');
412
+ return qs(containerSel)||qs('[data-stx-content]')||qs('main');
285
413
  }
286
414
 
287
415
  // \u2500\u2500 Navigation \u2500\u2500
@@ -365,7 +493,7 @@ export function getRouterScript() {
365
493
  // navigation. Scripts without an owner are unaffected.
366
494
  function ownerStays(owner){
367
495
  if(!owner)return false;
368
- var root=document.querySelector('[data-stx-scope="'+owner+'"]');
496
+ var root=qs('[data-stx-scope="'+owner+'"]');
369
497
  return !root||typeof root.__stx_disposers==='function';
370
498
  }
371
499
  function hashScript(code){
@@ -532,7 +660,7 @@ export function getRouterScript() {
532
660
  if(!key||loadedExternalScripts[key])return null;
533
661
  loadedExternalScripts[key]=1;
534
662
  return new Promise(function(resolve){
535
- var el=document.createElement('script');
663
+ var el=ce('script');
536
664
  el.src=key;
537
665
  // Deliberately NOT data-stx-page: both swap paths clear those on every
538
666
  // navigation, which would sweep this away and \u2014 since it is never loaded
@@ -541,12 +669,12 @@ export function getRouterScript() {
541
669
  el.setAttribute('data-stx-external','');
542
670
  el.onload=function(){resolve()};
543
671
  el.onerror=function(){log('[router] container script failed:',key);resolve()};
544
- document.head.appendChild(el);
672
+ dhead().appendChild(el);
545
673
  });
546
674
  }
547
675
 
548
676
  // Record scripts from the initial page load
549
- document.querySelectorAll('script').forEach(function(s){
677
+ qsa('script').forEach(function(s){
550
678
  var text=s.textContent||'';
551
679
  if(s.hasAttribute('src'))rememberExternalScript(s.getAttribute('src'));
552
680
  else if(text.trim())executedScriptHashes[hashScript(text)]=1;
@@ -573,17 +701,17 @@ export function getRouterScript() {
573
701
  return part?part.replace(/\\.stx$/i,''):'app';
574
702
  }
575
703
  function getCurrentLayoutGroup(){
576
- var meta=document.querySelector('meta[name="stx-layout-group"]');
704
+ var meta=qs(LAYOUT_GROUP_META);
577
705
  if(meta&&meta.getAttribute('content'))return meta.getAttribute('content');
578
- var layout=document.querySelector('meta[name="stx-layout"]');
706
+ var layout=qs(LAYOUT_META);
579
707
  return defaultLayoutGroup(layout?layout.getAttribute('content'):'');
580
708
  }
581
709
  function getDocLayoutGroup(doc,layout){
582
- var meta=doc&&doc.querySelector?doc.querySelector('meta[name="stx-layout-group"]'):null;
710
+ var meta=doc&&doc.querySelector?doc.querySelector(LAYOUT_GROUP_META):null;
583
711
  return meta&&meta.getAttribute('content')?meta.getAttribute('content'):defaultLayoutGroup(layout||'');
584
712
  }
585
713
  function checkLayoutChange(newLayout,targetUrl,newGroup){
586
- var currentLayout=document.querySelector('meta[name="stx-layout"]');
714
+ var currentLayout=qs(LAYOUT_META);
587
715
  var curLayoutName=currentLayout?currentLayout.getAttribute('content'):'';
588
716
  var curGroup=getCurrentLayoutGroup();
589
717
  var nextGroup=newGroup||defaultLayoutGroup(newLayout);
@@ -612,9 +740,14 @@ export function getRouterScript() {
612
740
  // through swap() to each of the three history sites would have been the
613
741
  // alternative (#1807).
614
742
  function writeHistory(mode,href){
615
- if(mode==='replace')history.replaceState({},'',href);
616
- else history.pushState({},'',href);
743
+ // The position belongs to the entry being left, and scrollToken still
744
+ // names it here.
745
+ rememberScroll();
746
+ if(mode==='replace'){history.replaceState(tokenState(),'',href);return}
747
+ scrollToken=newScrollToken();
748
+ history.pushState(tokenState(),'',href);
617
749
  }
750
+ function tokenState(){var state={};state[SCROLL_TOKEN]=scrollToken;return state}
618
751
 
619
752
  // Second arg accepts the legacy pushState boolean OR an options object
620
753
  // { replace }. Callers inside this file still pass the boolean.
@@ -636,7 +769,7 @@ export function getRouterScript() {
636
769
 
637
770
  if(t.pathname===location.pathname&&t.hash){
638
771
  if(pushState!==false)writeHistory(pushState,t.href);
639
- var el=document.querySelector(t.hash);
772
+ var el=qs(t.hash);
640
773
  if(el)el.scrollIntoView({behavior:'smooth'});
641
774
  return Promise.resolve(true);
642
775
  }
@@ -781,7 +914,7 @@ else {
781
914
  function routeAnnouncer(){
782
915
  var el=document.getElementById('stx-route-announcer');
783
916
  if(el)return el;
784
- el=document.createElement('div');
917
+ el=ce('div');
785
918
  el.id='stx-route-announcer';
786
919
  el.setAttribute('aria-live','polite');
787
920
  el.setAttribute('role','status');
@@ -814,7 +947,7 @@ else {
814
947
  function focusAfterNavigation(hash){
815
948
  if(o.routeFocus===false)return;
816
949
  var target=null;
817
- if(hash){try{target=document.querySelector(hash)}catch(e){target=null}}
950
+ if(hash){try{target=qs(hash)}catch(e){target=null}}
818
951
  if(!target)target=getContainer();
819
952
  if(!target||!target.focus)return;
820
953
  // The container is not naturally focusable. tabindex="-1" makes it
@@ -837,7 +970,7 @@ else {
837
970
  // be the last one, every navigation, not just the first time it is seen.
838
971
  function findGeneratedCss(abs){
839
972
  var found=null;
840
- document.querySelectorAll('head link[data-css][href]').forEach(function(link){
973
+ qsa('head link[data-css][href]').forEach(function(link){
841
974
  if(new URL(link.getAttribute('href'),location.href).href!==abs)return;
842
975
  // Collapse duplicates, so moving one copy to the end can never leave an
843
976
  // older copy of the same sheet behind to fight it.
@@ -866,7 +999,7 @@ else {
866
999
  if(link.__stxCssReady)pending.push(link.__stxCssReady);
867
1000
  return;
868
1001
  }
869
- link=document.createElement('link');
1002
+ link=ce('link');
870
1003
  link.setAttribute('data-css','generated');
871
1004
  link.setAttribute('rel','stylesheet');
872
1005
  link.setAttribute('href',new URL(href,location.href).href===abs?href:abs);
@@ -887,7 +1020,7 @@ else {
887
1020
  link.onerror=function(){finish(true)};
888
1021
  });
889
1022
  pending.push(link.__stxCssReady);
890
- document.head.appendChild(link);
1023
+ dhead().appendChild(link);
891
1024
  });
892
1025
  return pending.length?Promise.all(pending).then(function(results){return results.every(function(ok){return ok!==false})}):null;
893
1026
  }
@@ -923,7 +1056,7 @@ else {
923
1056
  link.removeAttribute('data-stx-css-pending');
924
1057
  link.removeAttribute('media');
925
1058
  }
926
- document.head.appendChild(link);
1059
+ dhead().appendChild(link);
927
1060
  });
928
1061
  }
929
1062
 
@@ -1036,18 +1169,18 @@ else {
1036
1169
  });
1037
1170
  cleanFrag=cleanFrag.replace(new RegExp('<!--stx-held-script-(\\\\d+)-->','g'),function(m,i){return heldScripts[Number(i)]});
1038
1171
  // Remove old page styles (not css \u2014 that gets merged)
1039
- document.querySelectorAll('style[data-stx-page]').forEach(function(s){s.remove()});
1172
+ qsa('style[data-stx-page]').forEach(function(s){s.remove()});
1040
1173
  // Merge css CSS from fragment into existing css style
1041
1174
  if(fragCss){
1042
- var curCss=document.querySelector('head style[data-css]');
1175
+ var curCss=qs('head style[data-css]');
1043
1176
  if(curCss){
1044
1177
  var merged=mergeCss(curCss.textContent||'',fragCss);
1045
1178
  if(merged)curCss.textContent=merged;
1046
1179
  }else{
1047
- var cw=document.createElement('style');
1180
+ var cw=ce('style');
1048
1181
  cw.setAttribute('data-css','generated');
1049
1182
  cw.textContent=fragCss;
1050
- document.head.appendChild(cw);
1183
+ dhead().appendChild(cw);
1051
1184
  }
1052
1185
  }
1053
1186
  // Already present ones are MOVED last, not skipped \u2014 see
@@ -1055,10 +1188,10 @@ else {
1055
1188
  promoteGeneratedCss(fragCssHrefs,location.href);
1056
1189
  // Add new page styles
1057
1190
  fragStyles.forEach(function(s){
1058
- var el=document.createElement('style');
1191
+ var el=ce('style');
1059
1192
  el.textContent=s.css;
1060
1193
  el.setAttribute('data-stx-page','');
1061
- document.head.appendChild(el);
1194
+ dhead().appendChild(el);
1062
1195
  });
1063
1196
  // Swap content \u2014 apply the destination container's own attributes first
1064
1197
  // so the incoming markup lands in a correctly-laid-out container instead
@@ -1066,11 +1199,10 @@ else {
1066
1199
  applyContainerAttrs(currentContent,pendingContainerAttrs);
1067
1200
  currentContent.innerHTML=cleanFrag;
1068
1201
  // Remove old page scripts
1069
- document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
1202
+ qsa('script[data-stx-page]').forEach(function(s){s.remove()});
1070
1203
  if(pushState!==false)writeHistory(pushState,url+(hash||''));
1071
1204
  refreshCurrentLinks();
1072
- if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'});
1073
- else if(hash){var el=document.querySelector(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
1205
+ applyScroll(hash);
1074
1206
  window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
1075
1207
  // Before page scripts run, so a page that focuses its own control on
1076
1208
  // mount still wins \u2014 its script executes after this.
@@ -1080,7 +1212,7 @@ else {
1080
1212
  setTimeout(announceRoute,0);
1081
1213
  // Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
1082
1214
  log('[router] frag scripts:', fragScripts.length);
1083
- document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
1215
+ qsa('script[data-stx-page]').forEach(function(s){s.remove()});
1084
1216
  // The page's module registry first (#1957), setups last. A fragment
1085
1217
  // lists its scripts in document order, and the registry is emitted
1086
1218
  // after the content it serves: a component that imports a package ran
@@ -1092,11 +1224,11 @@ else {
1092
1224
  fragScripts.forEach(function(entry){
1093
1225
  var code=entry.text;
1094
1226
  log('[router] exec script len:', code.length, 'has __stx_setup:', code.indexOf('__stx_setup')>-1);
1095
- var placeholder=document.querySelector('script[data-stx-route-script="'+entry.slot+'"]');
1227
+ var placeholder=qs('script[data-stx-route-script="'+entry.slot+'"]');
1096
1228
  var nestedScopedTemplate=false;
1097
1229
  var scopedLoopSetup=false;
1098
1230
  if(entry.scoped&&placeholder){
1099
- document.querySelectorAll('template').forEach(function(template){
1231
+ qsa('template').forEach(function(template){
1100
1232
  if(template.contains(placeholder)||(template.content&&template.content.contains(placeholder)))
1101
1233
  nestedScopedTemplate=true;
1102
1234
  });
@@ -1139,7 +1271,7 @@ else {
1139
1271
  return;
1140
1272
  }
1141
1273
  executedScriptHashes[h]=1;
1142
- var ns=document.createElement('script');
1274
+ var ns=ce('script');
1143
1275
  var hasImport=hasStaticImport(code);
1144
1276
  if(hasImport){
1145
1277
  ns.type='module';
@@ -1202,7 +1334,7 @@ else {
1202
1334
  var doc=parser.parseFromString(html,'text/html');
1203
1335
  // Full documents carry the build id as a meta rather than a header, and
1204
1336
  // this path also serves cached HTML, so check here too (#1772).
1205
- var docBuildMeta=doc.querySelector('meta[name="stx-build"]');
1337
+ var docBuildMeta=doc.querySelector(BUILD_META);
1206
1338
  var docBuild=docBuildMeta?(docBuildMeta.getAttribute('content')||''):'';
1207
1339
  if(isBuildSkew(docBuild)){reloadForSkew(url,docBuild);return Promise.resolve(false)}
1208
1340
  // Same hand-off as the fragment path, for the same reason (#1809, #1839).
@@ -1236,12 +1368,12 @@ else {
1236
1368
  // \u2500\u2500 Swap <head> styles \u2500\u2500
1237
1369
  // Inject new styles FIRST, then remove old to prevent unstyled flash
1238
1370
  var keepIds={'stx-view-transitions':1,'stx-r-css':1};
1239
- var curStyles=document.querySelectorAll('head style');
1371
+ var curStyles=qsa('head style');
1240
1372
  var newStyles=doc.querySelectorAll('head style');
1241
1373
 
1242
1374
  // Merge css styles instead of replacing \u2014 persistent elements
1243
1375
  // (nav, footer) outside <main> still need their utility classes
1244
- var curCss=document.querySelector('head style[data-css]');
1376
+ var curCss=qs('head style[data-css]');
1245
1377
  var newCss=null;
1246
1378
  newStyles.forEach(function(s){if(s.getAttribute('data-css'))newCss=s});
1247
1379
 
@@ -1253,20 +1385,20 @@ else {
1253
1385
  var incoming=[];
1254
1386
  newStyles.forEach(function(s){
1255
1387
  if(!keepIds[s.id]&&!s.getAttribute('data-css')){
1256
- var ns=document.createElement('style');
1388
+ var ns=ce('style');
1257
1389
  ns.textContent=s.textContent;
1258
1390
  ns.setAttribute('data-stx-incoming','');
1259
- document.head.appendChild(ns);
1391
+ dhead().appendChild(ns);
1260
1392
  incoming.push(ns);
1261
1393
  }
1262
1394
  });
1263
1395
 
1264
1396
  // If no existing css but new page has one, add it
1265
1397
  if(!curCss&&newCss){
1266
- var ns=document.createElement('style');
1398
+ var ns=ce('style');
1267
1399
  ns.textContent=newCss.textContent;
1268
1400
  ns.setAttribute('data-css',newCss.getAttribute('data-css'));
1269
- document.head.appendChild(ns);
1401
+ dhead().appendChild(ns);
1270
1402
  }
1271
1403
 
1272
1404
  // Remove old styles (except persistent ones, css, and incoming)
@@ -1293,7 +1425,7 @@ else {
1293
1425
  try{docBase=new URL(url,location.href).href}catch(e){docBase=location.href}
1294
1426
  var linkSel='link[rel="stylesheet"],link[rel="preconnect"]';
1295
1427
  var curLinks={};
1296
- document.querySelectorAll('head '+linkSel).forEach(function(l){var h=l.getAttribute('href');if(h)curLinks[new URL(h,location.href).href]=1});
1428
+ qsa('head '+linkSel).forEach(function(l){var h=l.getAttribute('href');if(h)curLinks[new URL(h,location.href).href]=1});
1297
1429
  doc.querySelectorAll('head '+linkSel).forEach(function(l){
1298
1430
  var href=l.getAttribute('href');
1299
1431
  if(!href)return;
@@ -1303,9 +1435,9 @@ else {
1303
1435
  var abs=new URL(href,docBase).href;
1304
1436
  if(curLinks[abs])return;
1305
1437
  curLinks[abs]=1;
1306
- var nl=document.createElement('link');
1438
+ var nl=ce('link');
1307
1439
  Array.from(l.attributes).forEach(function(a){nl.setAttribute(a.name,a.value)});
1308
- document.head.appendChild(nl);
1440
+ dhead().appendChild(nl);
1309
1441
  });
1310
1442
  // After every other stylesheet link, so the destination's complete
1311
1443
  // utility sheet is last in the cascade.
@@ -1319,8 +1451,8 @@ else {
1319
1451
  var declaredLayout=pendingLayoutDecl;
1320
1452
  pendingLayoutDecl=null;
1321
1453
  if(newBody){
1322
- var newMeta=doc.querySelector('meta[name="stx-layout"]');
1323
- var curMeta=document.querySelector('meta[name="stx-layout"]');
1454
+ var newMeta=doc.querySelector(LAYOUT_META);
1455
+ var curMeta=qs(LAYOUT_META);
1324
1456
  var curLayout=curMeta?curMeta.getAttribute('content'):'';
1325
1457
  // The server's answer wins over the document's metas. We are only here
1326
1458
  // because checkLayoutChange already said the group changed, based on
@@ -1388,16 +1520,16 @@ else {
1388
1520
  // Copy body attributes (class, data-stx, etc.)
1389
1521
  Array.from(newBody.attributes).forEach(function(attr){document.body.setAttribute(attr.name,attr.value)});
1390
1522
  // Update layout meta tag
1391
- var oldMeta=document.querySelector('meta[name="stx-layout"]');
1392
- var freshMeta=doc.querySelector('meta[name="stx-layout"]');
1523
+ var oldMeta=qs(LAYOUT_META);
1524
+ var freshMeta=doc.querySelector(LAYOUT_META);
1393
1525
  if(oldMeta&&freshMeta)oldMeta.setAttribute('content',freshMeta.getAttribute('content')||'');
1394
- else if(freshMeta){var m=document.createElement('meta');m.name='stx-layout';m.content=freshMeta.getAttribute('content')||'';document.head.appendChild(m)}
1395
- var oldGroupMeta=document.querySelector('meta[name="stx-layout-group"]');
1396
- var freshGroupMeta=doc.querySelector('meta[name="stx-layout-group"]');
1526
+ else if(freshMeta){var m=ce('meta');m.name='stx-layout';m.content=freshMeta.getAttribute('content')||'';dhead().appendChild(m)}
1527
+ var oldGroupMeta=qs(LAYOUT_GROUP_META);
1528
+ var freshGroupMeta=doc.querySelector(LAYOUT_GROUP_META);
1397
1529
  if(oldGroupMeta&&freshGroupMeta)oldGroupMeta.setAttribute('content',freshGroupMeta.getAttribute('content')||'');
1398
- else if(freshGroupMeta){var gm=document.createElement('meta');gm.name='stx-layout-group';gm.content=freshGroupMeta.getAttribute('content')||'';document.head.appendChild(gm)}
1530
+ else if(freshGroupMeta){var gm=ce('meta');gm.name='stx-layout-group';gm.content=freshGroupMeta.getAttribute('content')||'';dhead().appendChild(gm)}
1399
1531
  // Update container reference for script execution below
1400
- currentContent=document.querySelector(containerSel)||document.querySelector('main')||document.body;
1532
+ currentContent=qs(containerSel)||qs('main')||document.body;
1401
1533
  } else {
1402
1534
  // Same layout \u2014 swap only container content. Carry the destination
1403
1535
  // container's attributes across too (same reason as the fragment path:
@@ -1416,7 +1548,7 @@ else {
1416
1548
 
1417
1549
  // \u2500\u2500 Load new external <head> scripts \u2500\u2500
1418
1550
  var loadedSrcs={};
1419
- document.querySelectorAll('head script[src]').forEach(function(s){loadedSrcs[s.src]=1});
1551
+ qsa('head script[src]').forEach(function(s){loadedSrcs[s.src]=1});
1420
1552
  var extPromises=[];
1421
1553
  doc.querySelectorAll('head script[src]').forEach(function(s){
1422
1554
  if(isSharedClientScript(s))return;
@@ -1426,11 +1558,11 @@ else {
1426
1558
  if(loadedSrcs[src])return;
1427
1559
  loadedSrcs[src]=1;
1428
1560
  extPromises.push(new Promise(function(resolve,reject){
1429
- var ns=document.createElement('script');
1561
+ var ns=ce('script');
1430
1562
  ns.src=src;
1431
1563
  ns.onload=resolve;
1432
1564
  ns.onerror=reject;
1433
- document.head.appendChild(ns);
1565
+ dhead().appendChild(ns);
1434
1566
  }));
1435
1567
  });
1436
1568
 
@@ -1444,7 +1576,7 @@ else {
1444
1576
 
1445
1577
  // \u2500\u2500 Script re-execution \u2500\u2500
1446
1578
  // Remove previously injected page scripts
1447
- document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
1579
+ qsa('script[data-stx-page]').forEach(function(s){s.remove()});
1448
1580
 
1449
1581
  var scripts=routedBodyScripts.slice();
1450
1582
  function addScript(text, runAlways, slot, owner){
@@ -1528,8 +1660,7 @@ else {
1528
1660
  refreshCurrentLinks();
1529
1661
 
1530
1662
  // Scroll
1531
- if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'});
1532
- else if(hash){var el=document.querySelector(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
1663
+ applyScroll(hash);
1533
1664
 
1534
1665
  // Update title
1535
1666
  var newTitle=doc.querySelector('title');
@@ -1590,7 +1721,7 @@ else {
1590
1721
  if(!runAlways&&!isSetup&&executedScriptHashes[h])return;
1591
1722
  executedScriptHashes[h]=1;
1592
1723
  // Wrap scripts with import statements as modules (Bug 3 fix)
1593
- var ns=document.createElement('script');
1724
+ var ns=ce('script');
1594
1725
  if(hasImport){
1595
1726
  ns.type='module';
1596
1727
  }
@@ -1603,7 +1734,7 @@ else {
1603
1734
  var alreadyScoped=runsAlways(typeof entry==='string'?'':entry.run,text);
1604
1735
  ns.textContent=(hasImport||alreadyScoped)?text:'{'+text+'}';
1605
1736
  ns.setAttribute('data-stx-page','');
1606
- var placeholder=entry.slot?document.querySelector('script[data-stx-route-script="'+entry.slot+'"]'):null;
1737
+ var placeholder=entry.slot?qs('script[data-stx-route-script="'+entry.slot+'"]'):null;
1607
1738
  if(placeholder&&placeholder.parentNode){
1608
1739
  ns.setAttribute('data-stx-positioned','');
1609
1740
  placeholder.parentNode.replaceChild(ns,placeholder);
@@ -1895,6 +2026,16 @@ else {
1895
2026
 
1896
2027
  // \u2500\u2500 Back/forward \u2500\u2500
1897
2028
  window.addEventListener('popstate',function(){
2029
+ // Nothing has scrolled yet (restoration is manual), so the viewport still
2030
+ // shows the entry being left: save it under its own token before adopting
2031
+ // the popped entry's, and hand the swap the position to land on.
2032
+ rememberScroll();
2033
+ if(restoreScroll){
2034
+ var popped=history.state&&history.state[SCROLL_TOKEN];
2035
+ scrollToken=popped||newScrollToken();
2036
+ if(!popped)stampScrollToken();
2037
+ pendingScroll=readScroll(scrollToken);
2038
+ }
1898
2039
  navigate(location.pathname+location.search+location.hash,false);
1899
2040
  });
1900
2041
 
@@ -1961,7 +2102,7 @@ else {
1961
2102
  }
1962
2103
 
1963
2104
  function updateNav(){
1964
- document.querySelectorAll('nav a[href], #mobileNav a[href], [data-stx-nav] a[href]').forEach(function(a){
2105
+ qsa('nav a[href], #mobileNav a[href], [data-stx-nav] a[href]').forEach(function(a){
1965
2106
  if(!a.hasAttribute('data-stx-link'))return;
1966
2107
  var st=linkState(a.getAttribute('href'),a.getAttribute('data-stx-active-match'));
1967
2108
  if(!st)return;
@@ -1982,7 +2123,7 @@ else {
1982
2123
 
1983
2124
  function updateActiveLinks(){
1984
2125
  // Update active classes on <stx-link> elements (and legacy data-stx-link)
1985
- var links=document.querySelectorAll('[data-stx-link]');
2126
+ var links=qsa('[data-stx-link]');
1986
2127
  links.forEach(function(link){
1987
2128
  var st=linkState(link.getAttribute('to')||link.getAttribute('href')||'',link.getAttribute('data-stx-active-match'));
1988
2129
  if(!st)return;
@@ -2024,7 +2165,7 @@ else {
2024
2165
  // the cascade, or opt out entirely with viewTransitions:false.
2025
2166
  function injectStyles(){
2026
2167
  if(!document.getElementById('stx-r-css')){
2027
- var s=document.createElement('style');s.id='stx-r-css';
2168
+ var s=ce('style');s.id='stx-r-css';
2028
2169
  // Strip characters that could escape our CSS block and inject new
2029
2170
  // declarations: ; { } ( ) " ' \\ < > plus whitespace control chars.
2030
2171
  // This keeps the value safe to concat into a stylesheet even if a
@@ -2046,10 +2187,10 @@ else {
2046
2187
  css+='@media (prefers-reduced-motion: reduce){::view-transition-old(root),::view-transition-new(root){animation-duration:0s;animation-name:none}}';
2047
2188
  }
2048
2189
  s.textContent=css;
2049
- document.head.appendChild(s);
2190
+ dhead().appendChild(s);
2050
2191
  }
2051
2192
  if(o.progress&&!document.getElementById('stx-router-progress')){
2052
- var el=document.createElement('div');
2193
+ var el=ce('div');
2053
2194
  el.id='stx-router-progress';
2054
2195
  el.setAttribute('role','progressbar');
2055
2196
  el.setAttribute('aria-hidden','true');
@@ -2066,7 +2207,7 @@ else {
2066
2207
 
2067
2208
  function injectViewTransitionCSS(){
2068
2209
  if(document.getElementById('stx-view-transitions'))return;
2069
- var s=document.createElement('style');s.id='stx-view-transitions';
2210
+ var s=ce('style');s.id='stx-view-transitions';
2070
2211
  s.textContent='::view-transition-group(root){animation:none}::view-transition-old(root){animation:none}::view-transition-new(root){animation:none}main,#app-content,[data-stx-content]{view-transition-name:stx-content}::view-transition-old(stx-content){animation:stx-fade-out .15s ease-out both}::view-transition-new(stx-content){animation:stx-fade-in .15s ease-in .1s both}@keyframes stx-fade-out{from{opacity:1}to{opacity:0}}@keyframes stx-fade-in{from{opacity:0}to{opacity:1}}::view-transition{background:transparent}::view-transition-group(stx-content){background:inherit;overflow:hidden}';
2071
2212
  (document.head||document.documentElement).appendChild(s);
2072
2213
  }
@@ -2123,6 +2264,17 @@ else {
2123
2264
 
2124
2265
  if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
2125
2266
  })();
2126
- `);
2267
+ `;
2268
+ }
2269
+ export function getRouterScript() {
2270
+ if (cachedRouterScript)
2271
+ return cachedRouterScript;
2272
+ cachedRouterScript = minifyRouterScript(stripRouterLogs(routerSource()));
2127
2273
  return cachedRouterScript;
2128
2274
  }
2275
+ export function getRouterScriptDev() {
2276
+ if (cachedRouterScriptDev)
2277
+ return cachedRouterScriptDev;
2278
+ cachedRouterScriptDev = minifyRouterScript(routerSource());
2279
+ return cachedRouterScriptDev;
2280
+ }
package/dist/index.d.ts CHANGED
@@ -52,4 +52,4 @@ export { generateRouteTypes, generateRouteManifest } from './codegen';
52
52
  // Which files under a page root are public routes (#1866)
53
53
  export { NON_PAGE_DIRS, isNonPageRoutePath, isNonPageRoutePattern } from './page-routes';
54
54
  // Client-side SPA navigation
55
- export { getRouterScript } from './client';
55
+ export { getRouterScript, getRouterScriptDev } from './client';
package/dist/index.js CHANGED
@@ -22,4 +22,4 @@ export { resolveLayoutChain } from "./nested-layouts.js";
22
22
  export { deriveLayoutGroup, extractLayoutMetadata } from "./layout-metadata.js";
23
23
  export { generateRouteTypes, generateRouteManifest } from "./codegen.js";
24
24
  export { NON_PAGE_DIRS, isNonPageRoutePath, isNonPageRoutePattern } from "./page-routes.js";
25
- export { getRouterScript } from "./client.js";
25
+ export { getRouterScript, getRouterScriptDev } from "./client.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stx-router",
3
3
  "type": "module",
4
- "version": "0.2.337",
4
+ "version": "0.2.339",
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",