stx-router 0.2.151 → 0.2.153
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.js +95 -15
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -21,6 +21,28 @@ export function getRouterScript() {
|
|
|
21
21
|
var containerSel=o.container;
|
|
22
22
|
var debug=!!o.debug;
|
|
23
23
|
function log(){if(debug&&typeof console!=='undefined'&&console.log)console.log.apply(console,arguments)}
|
|
24
|
+
|
|
25
|
+
// \u2500\u2500 Build skew (stacksjs/stx#1772) \u2500\u2500
|
|
26
|
+
// The build that rendered THIS document, and therefore the build the runtime
|
|
27
|
+
// executing right now came from. Under bun --watch a save restarts the server,
|
|
28
|
+
// so the next navigation can fetch a fragment produced by a newer build; when
|
|
29
|
+
// the scoped-script or binding format drifted between the two, the old runtime
|
|
30
|
+
// cannot hydrate the new fragment \u2014 literal moustaches, dead bindings, stale
|
|
31
|
+
// canvas. Sporadic, and never on a clean boot.
|
|
32
|
+
//
|
|
33
|
+
// Conservative on purpose: act only when BOTH ids are known. A missing id is
|
|
34
|
+
// "no information", never "mismatch", so statically hosted output (no headers)
|
|
35
|
+
// and older servers behave exactly as before.
|
|
36
|
+
var buildMeta=document.querySelector('meta[name="stx-build"]');
|
|
37
|
+
var loadedBuild=buildMeta?(buildMeta.getAttribute('content')||''):'';
|
|
38
|
+
window.__stxBuild=loadedBuild;
|
|
39
|
+
function isBuildSkew(incoming){
|
|
40
|
+
return !!(loadedBuild&&incoming&&incoming!==loadedBuild);
|
|
41
|
+
}
|
|
42
|
+
function reloadForSkew(url,incoming){
|
|
43
|
+
log('[router] build skew: page is',loadedBuild,'server is',incoming,'\u2014 full navigation');
|
|
44
|
+
location.href=url;
|
|
45
|
+
}
|
|
24
46
|
function runViewTransition(callback){
|
|
25
47
|
if(!o.viewTransitions||!document.startViewTransition)return false;
|
|
26
48
|
try{
|
|
@@ -216,6 +238,27 @@ export function getRouterScript() {
|
|
|
216
238
|
// Layout-level scripts (theme, nav setup) execute on initial page load and
|
|
217
239
|
// should NOT re-execute when navigating to another page with the same layout.
|
|
218
240
|
var executedScriptHashes={};
|
|
241
|
+
|
|
242
|
+
// \u2500\u2500 Re-execution contract (stacksjs/stx#1773) \u2500\u2500
|
|
243
|
+
// Whether a generated script may run again after an SPA swap used to be
|
|
244
|
+
// SNIFFED from its source: does it start with '(' or ';', or mention
|
|
245
|
+
// window.stx.mount. That is a property of the emitted shape, not of intent,
|
|
246
|
+
// so every change to how scripts are wrapped was one edit away from silently
|
|
247
|
+
// reclassifying them \u2014 and the symptom of getting it wrong is a component
|
|
248
|
+
// that renders completely dead (literal moustaches, stuck :show) only on a
|
|
249
|
+
// REVISIT, because nav-away already disposed its scope.
|
|
250
|
+
//
|
|
251
|
+
// Emitters now declare it: data-stx-run="always" on self-contained scope
|
|
252
|
+
// IIFEs and mount() wrappers, which own their scope and whose registration
|
|
253
|
+
// _cleanupContainer removed on the way out. Anything unstamped falls back to
|
|
254
|
+
// the old sniff, so a page rendered by an older server behaves exactly as
|
|
255
|
+
// before.
|
|
256
|
+
function runsAlways(declared, code){
|
|
257
|
+
if(declared==='always')return true;
|
|
258
|
+
if(declared==='once')return false;
|
|
259
|
+
var head=code.trimStart();
|
|
260
|
+
return head.charAt(0)==='('||head.charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
|
|
261
|
+
}
|
|
219
262
|
function hashScript(code){
|
|
220
263
|
var h=0;for(var i=0;i<code.length;i++){h=((h<<5)-h)+code.charCodeAt(i);h|=0}
|
|
221
264
|
return h;
|
|
@@ -364,6 +407,10 @@ else {
|
|
|
364
407
|
var wantsFragment=shouldUseFragmentResponse();
|
|
365
408
|
return fetch(url,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
|
|
366
409
|
if(!r.ok)throw new Error(r.status);
|
|
410
|
+
// Before anything is parsed or swapped: a fragment from a different
|
|
411
|
+
// build must not be handed to this page's runtime (#1772).
|
|
412
|
+
var incomingBuild=r.headers.get('X-STX-Build')||'';
|
|
413
|
+
if(isBuildSkew(incomingBuild)){reloadForSkew(url,incomingBuild);return null}
|
|
367
414
|
var isFragment=wantsFragment&&r.headers.get('X-STX-Fragment')==='true';
|
|
368
415
|
var newLayout=r.headers.get('X-STX-Layout')||'';
|
|
369
416
|
var newGroup=r.headers.get('X-STX-Layout-Group')||'';
|
|
@@ -433,12 +480,15 @@ else {
|
|
|
433
480
|
if(code&&code.trim()&&!isSignalsRuntimeScript({hasAttribute:function(name){return name==='data-stx-runtime'&&attrs.indexOf('data-stx-runtime')!==-1}},code)){
|
|
434
481
|
var slot='fragment-'+(++fragScriptId);
|
|
435
482
|
var scoped=/(?:^|\\s)data-stx-scoped(?:\\s|=|$)/i.test(attrs);
|
|
436
|
-
|
|
483
|
+
var runDecl=(attrs.match(/data-stx-run\\s*=\\s*["']?(always|once)["']?/i)||[])[1];
|
|
484
|
+
fragScripts.push({text:code,slot:slot,setupName:generatedSetupName(code),scoped:scoped,run:runDecl?runDecl.toLowerCase():''});
|
|
437
485
|
// Retain scoped setup code in its inert placeholder. A placeholder
|
|
438
486
|
// inside template.content is unreachable through document, so the
|
|
439
487
|
// repeated component runtime must execute it for each clone.
|
|
440
488
|
return '<scr'+'ipt type="application/stx-pending" data-stx-route-script="'+slot+'"'
|
|
441
|
-
+(scoped?' data-stx-scoped':'')
|
|
489
|
+
+(scoped?' data-stx-scoped':'')
|
|
490
|
+
+(runDecl?' data-stx-run="'+runDecl.toLowerCase()+'"':'')
|
|
491
|
+
+'>'+code+'<\\/scr'+'ipt>';
|
|
442
492
|
}
|
|
443
493
|
return '';
|
|
444
494
|
});
|
|
@@ -541,17 +591,14 @@ else {
|
|
|
541
591
|
// re-execute because each page has its own setup.
|
|
542
592
|
var h=hashScript(code);
|
|
543
593
|
var isSetup=code.indexOf('__stx_setup_')!==-1;
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
// and all :if branches stay visible at once.
|
|
553
|
-
// Mirrors the full-doc swap path's runAlways escape below.
|
|
554
|
-
var isAlreadyScoped=code.trimStart().charAt(0)==='('||code.trimStart().charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
|
|
594
|
+
// Self-contained scope scripts must re-execute on every navigation:
|
|
595
|
+
// they own their scope, won't collide with prior runs, and
|
|
596
|
+
// _cleanupContainer deleted their entry from window.stx._scopes on
|
|
597
|
+
// the way out. Skipping one leaves the page leaf with no scope
|
|
598
|
+
// registered \u2014 bindIf/@event never re-bind and every :if branch
|
|
599
|
+
// stays visible at once. Declared by the emitter via data-stx-run,
|
|
600
|
+
// sniffed only as a fallback (#1773).
|
|
601
|
+
var isAlreadyScoped=runsAlways(entry.run,code);
|
|
555
602
|
if(!isSetup&&!isAlreadyScoped&&executedScriptHashes[h]){
|
|
556
603
|
log('[router] skipping already-executed script (hash dedup)');
|
|
557
604
|
return;
|
|
@@ -590,6 +637,11 @@ else {
|
|
|
590
637
|
// Full document mode: parse with DOMParser and extract container content
|
|
591
638
|
var parser=new DOMParser();
|
|
592
639
|
var doc=parser.parseFromString(html,'text/html');
|
|
640
|
+
// Full documents carry the build id as a meta rather than a header, and
|
|
641
|
+
// this path also serves cached HTML, so check here too (#1772).
|
|
642
|
+
var docBuildMeta=doc.querySelector('meta[name="stx-build"]');
|
|
643
|
+
var docBuild=docBuildMeta?(docBuildMeta.getAttribute('content')||''):'';
|
|
644
|
+
if(isBuildSkew(docBuild)){reloadForSkew(url,docBuild);return Promise.resolve(false)}
|
|
593
645
|
var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
|
|
594
646
|
if(!newContent){location.href=url;return Promise.resolve(false)}
|
|
595
647
|
|
|
@@ -703,7 +755,7 @@ else {
|
|
|
703
755
|
var slot='document-'+(++routedBodyScriptId);
|
|
704
756
|
var setupName=generatedSetupName(text);
|
|
705
757
|
if(setupName)incomingSetupName=setupName;
|
|
706
|
-
routedBodyScripts.push({text:text,runAlways:true,slot:slot,setupName:setupName});
|
|
758
|
+
routedBodyScripts.push({text:text,runAlways:true,slot:slot,setupName:setupName,run:s.getAttribute('data-stx-run')||''});
|
|
707
759
|
s.textContent='';
|
|
708
760
|
s.setAttribute('type','application/stx-pending');
|
|
709
761
|
s.setAttribute('data-stx-route-script',slot);
|
|
@@ -831,6 +883,34 @@ else {
|
|
|
831
883
|
document.documentElement.lang=doc.documentElement.lang;
|
|
832
884
|
}
|
|
833
885
|
|
|
886
|
+
// \u2500\u2500 Reconcile <html> attributes (stacksjs/stx#1798) \u2500\u2500
|
|
887
|
+
// A layout that scopes its design tokens to the root element
|
|
888
|
+
// (html.marketing { --bg: \u2026 }) needs that class to LEAVE when you
|
|
889
|
+
// navigate to a layout that doesn't want it \u2014 otherwise both token sets
|
|
890
|
+
// match at once and the second layout paints with the first one's
|
|
891
|
+
// palette. Head stylesheets are additive across a swap, so the class is
|
|
892
|
+
// the only thing disambiguating them.
|
|
893
|
+
//
|
|
894
|
+
// Only what stx wrote is touched, per the markers emitted by
|
|
895
|
+
// document-shell.ts. Diffing the whole element against the incoming
|
|
896
|
+
// document would strip the color-mode boot's dark class and
|
|
897
|
+
// data-reduced-motion, which exist only on the live page.
|
|
898
|
+
if(doc.documentElement){
|
|
899
|
+
var curRoot=document.documentElement,incRoot=doc.documentElement;
|
|
900
|
+
var tokens=function(el,attr){var v=el.getAttribute(attr);return v?v.split(/\\s+/).filter(Boolean):[]};
|
|
901
|
+
var prevCls=tokens(curRoot,'data-stx-html-class'),nextCls=tokens(incRoot,'data-stx-html-class');
|
|
902
|
+
prevCls.forEach(function(c){if(nextCls.indexOf(c)===-1)curRoot.classList.remove(c)});
|
|
903
|
+
nextCls.forEach(function(c){curRoot.classList.add(c)});
|
|
904
|
+
if(nextCls.length)curRoot.setAttribute('data-stx-html-class',nextCls.join(' '));
|
|
905
|
+
else curRoot.removeAttribute('data-stx-html-class');
|
|
906
|
+
|
|
907
|
+
var prevNames=tokens(curRoot,'data-stx-html-attrs'),nextNames=tokens(incRoot,'data-stx-html-attrs');
|
|
908
|
+
prevNames.forEach(function(n){if(nextNames.indexOf(n)===-1)curRoot.removeAttribute(n)});
|
|
909
|
+
nextNames.forEach(function(n){var v=incRoot.getAttribute(n);if(v!==null)curRoot.setAttribute(n,v)});
|
|
910
|
+
if(nextNames.length)curRoot.setAttribute('data-stx-html-attrs',nextNames.join(' '));
|
|
911
|
+
else curRoot.removeAttribute('data-stx-html-attrs');
|
|
912
|
+
}
|
|
913
|
+
|
|
834
914
|
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
|
|
835
915
|
|
|
836
916
|
// Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
|
|
@@ -860,7 +940,7 @@ else {
|
|
|
860
940
|
// Skip the wrap for module scripts \u2014 they get their own scope from
|
|
861
941
|
// ESM, and top-level 'import' is illegal inside a block, which
|
|
862
942
|
// would throw SyntaxError before the script ever runs.
|
|
863
|
-
var alreadyScoped=
|
|
943
|
+
var alreadyScoped=runsAlways(typeof entry==='string'?'':entry.run,text);
|
|
864
944
|
ns.textContent=(hasImport||alreadyScoped)?text:'{'+text+'}';
|
|
865
945
|
ns.setAttribute('data-stx-page','');
|
|
866
946
|
var placeholder=entry.slot?document.querySelector('script[data-stx-route-script="'+entry.slot+'"]'):null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stx-router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.153",
|
|
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",
|