bun-plugin-stx 0.2.152 → 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.
@@ -621,6 +621,28 @@ function getRouterScript() {
621
621
  var containerSel=o.container;
622
622
  var debug=!!o.debug;
623
623
  function log(){if(debug&&typeof console!=='undefined'&&console.log)console.log.apply(console,arguments)}
624
+
625
+ // \u2500\u2500 Build skew (stacksjs/stx#1772) \u2500\u2500
626
+ // The build that rendered THIS document, and therefore the build the runtime
627
+ // executing right now came from. Under bun --watch a save restarts the server,
628
+ // so the next navigation can fetch a fragment produced by a newer build; when
629
+ // the scoped-script or binding format drifted between the two, the old runtime
630
+ // cannot hydrate the new fragment \u2014 literal moustaches, dead bindings, stale
631
+ // canvas. Sporadic, and never on a clean boot.
632
+ //
633
+ // Conservative on purpose: act only when BOTH ids are known. A missing id is
634
+ // "no information", never "mismatch", so statically hosted output (no headers)
635
+ // and older servers behave exactly as before.
636
+ var buildMeta=document.querySelector('meta[name="stx-build"]');
637
+ var loadedBuild=buildMeta?(buildMeta.getAttribute('content')||''):'';
638
+ window.__stxBuild=loadedBuild;
639
+ function isBuildSkew(incoming){
640
+ return !!(loadedBuild&&incoming&&incoming!==loadedBuild);
641
+ }
642
+ function reloadForSkew(url,incoming){
643
+ log('[router] build skew: page is',loadedBuild,'server is',incoming,'\u2014 full navigation');
644
+ location.href=url;
645
+ }
624
646
  function runViewTransition(callback){
625
647
  if(!o.viewTransitions||!document.startViewTransition)return false;
626
648
  try{
@@ -816,6 +838,27 @@ function getRouterScript() {
816
838
  // Layout-level scripts (theme, nav setup) execute on initial page load and
817
839
  // should NOT re-execute when navigating to another page with the same layout.
818
840
  var executedScriptHashes={};
841
+
842
+ // \u2500\u2500 Re-execution contract (stacksjs/stx#1773) \u2500\u2500
843
+ // Whether a generated script may run again after an SPA swap used to be
844
+ // SNIFFED from its source: does it start with '(' or ';', or mention
845
+ // window.stx.mount. That is a property of the emitted shape, not of intent,
846
+ // so every change to how scripts are wrapped was one edit away from silently
847
+ // reclassifying them \u2014 and the symptom of getting it wrong is a component
848
+ // that renders completely dead (literal moustaches, stuck :show) only on a
849
+ // REVISIT, because nav-away already disposed its scope.
850
+ //
851
+ // Emitters now declare it: data-stx-run="always" on self-contained scope
852
+ // IIFEs and mount() wrappers, which own their scope and whose registration
853
+ // _cleanupContainer removed on the way out. Anything unstamped falls back to
854
+ // the old sniff, so a page rendered by an older server behaves exactly as
855
+ // before.
856
+ function runsAlways(declared, code){
857
+ if(declared==='always')return true;
858
+ if(declared==='once')return false;
859
+ var head=code.trimStart();
860
+ return head.charAt(0)==='('||head.charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
861
+ }
819
862
  function hashScript(code){
820
863
  var h=0;for(var i=0;i<code.length;i++){h=((h<<5)-h)+code.charCodeAt(i);h|=0}
821
864
  return h;
@@ -964,6 +1007,10 @@ else {
964
1007
  var wantsFragment=shouldUseFragmentResponse();
965
1008
  return fetch(url,{headers:wantsFragment?{'X-STX-Router':'true','Accept':'text/html'}:{'Accept':'text/html'}}).then(function(r){
966
1009
  if(!r.ok)throw new Error(r.status);
1010
+ // Before anything is parsed or swapped: a fragment from a different
1011
+ // build must not be handed to this page's runtime (#1772).
1012
+ var incomingBuild=r.headers.get('X-STX-Build')||'';
1013
+ if(isBuildSkew(incomingBuild)){reloadForSkew(url,incomingBuild);return null}
967
1014
  var isFragment=wantsFragment&&r.headers.get('X-STX-Fragment')==='true';
968
1015
  var newLayout=r.headers.get('X-STX-Layout')||'';
969
1016
  var newGroup=r.headers.get('X-STX-Layout-Group')||'';
@@ -1033,12 +1080,15 @@ else {
1033
1080
  if(code&&code.trim()&&!isSignalsRuntimeScript({hasAttribute:function(name){return name==='data-stx-runtime'&&attrs.indexOf('data-stx-runtime')!==-1}},code)){
1034
1081
  var slot='fragment-'+(++fragScriptId);
1035
1082
  var scoped=/(?:^|\\s)data-stx-scoped(?:\\s|=|$)/i.test(attrs);
1036
- fragScripts.push({text:code,slot:slot,setupName:generatedSetupName(code),scoped:scoped});
1083
+ var runDecl=(attrs.match(/data-stx-run\\s*=\\s*["']?(always|once)["']?/i)||[])[1];
1084
+ fragScripts.push({text:code,slot:slot,setupName:generatedSetupName(code),scoped:scoped,run:runDecl?runDecl.toLowerCase():''});
1037
1085
  // Retain scoped setup code in its inert placeholder. A placeholder
1038
1086
  // inside template.content is unreachable through document, so the
1039
1087
  // repeated component runtime must execute it for each clone.
1040
1088
  return '<scr'+'ipt type="application/stx-pending" data-stx-route-script="'+slot+'"'
1041
- +(scoped?' data-stx-scoped':'')+'>'+code+'<\\/scr'+'ipt>';
1089
+ +(scoped?' data-stx-scoped':'')
1090
+ +(runDecl?' data-stx-run="'+runDecl.toLowerCase()+'"':'')
1091
+ +'>'+code+'<\\/scr'+'ipt>';
1042
1092
  }
1043
1093
  return '';
1044
1094
  });
@@ -1141,17 +1191,14 @@ else {
1141
1191
  // re-execute because each page has its own setup.
1142
1192
  var h=hashScript(code);
1143
1193
  var isSetup=code.indexOf('__stx_setup_')!==-1;
1144
- // Compute isAlreadyScoped first so the dedup check can exempt
1145
- // self-contained IIFE scripts. data-stx-scoped scope IIFEs
1146
- // (the (function(){ ... })() shape) must re-execute on every
1147
- // navigation. They own their own scope and won't collide
1148
- // with prior runs, AND _cleanupContainer deleted their
1149
- // entry from window.stx._scopes on the way out. Without
1150
- // this exemption, navigating away and back leaves the page
1151
- // leaf with no scope registered: bindIf/@event never re-bind
1152
- // and all :if branches stay visible at once.
1153
- // Mirrors the full-doc swap path's runAlways escape below.
1154
- var isAlreadyScoped=code.trimStart().charAt(0)==='('||code.trimStart().charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
1194
+ // Self-contained scope scripts must re-execute on every navigation:
1195
+ // they own their scope, won't collide with prior runs, and
1196
+ // _cleanupContainer deleted their entry from window.stx._scopes on
1197
+ // the way out. Skipping one leaves the page leaf with no scope
1198
+ // registered \u2014 bindIf/@event never re-bind and every :if branch
1199
+ // stays visible at once. Declared by the emitter via data-stx-run,
1200
+ // sniffed only as a fallback (#1773).
1201
+ var isAlreadyScoped=runsAlways(entry.run,code);
1155
1202
  if(!isSetup&&!isAlreadyScoped&&executedScriptHashes[h]){
1156
1203
  log('[router] skipping already-executed script (hash dedup)');
1157
1204
  return;
@@ -1190,6 +1237,11 @@ else {
1190
1237
  // Full document mode: parse with DOMParser and extract container content
1191
1238
  var parser=new DOMParser();
1192
1239
  var doc=parser.parseFromString(html,'text/html');
1240
+ // Full documents carry the build id as a meta rather than a header, and
1241
+ // this path also serves cached HTML, so check here too (#1772).
1242
+ var docBuildMeta=doc.querySelector('meta[name="stx-build"]');
1243
+ var docBuild=docBuildMeta?(docBuildMeta.getAttribute('content')||''):'';
1244
+ if(isBuildSkew(docBuild)){reloadForSkew(url,docBuild);return Promise.resolve(false)}
1193
1245
  var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
1194
1246
  if(!newContent){location.href=url;return Promise.resolve(false)}
1195
1247
 
@@ -1303,7 +1355,7 @@ else {
1303
1355
  var slot='document-'+(++routedBodyScriptId);
1304
1356
  var setupName=generatedSetupName(text);
1305
1357
  if(setupName)incomingSetupName=setupName;
1306
- routedBodyScripts.push({text:text,runAlways:true,slot:slot,setupName:setupName});
1358
+ routedBodyScripts.push({text:text,runAlways:true,slot:slot,setupName:setupName,run:s.getAttribute('data-stx-run')||''});
1307
1359
  s.textContent='';
1308
1360
  s.setAttribute('type','application/stx-pending');
1309
1361
  s.setAttribute('data-stx-route-script',slot);
@@ -1431,6 +1483,34 @@ else {
1431
1483
  document.documentElement.lang=doc.documentElement.lang;
1432
1484
  }
1433
1485
 
1486
+ // \u2500\u2500 Reconcile <html> attributes (stacksjs/stx#1798) \u2500\u2500
1487
+ // A layout that scopes its design tokens to the root element
1488
+ // (html.marketing { --bg: \u2026 }) needs that class to LEAVE when you
1489
+ // navigate to a layout that doesn't want it \u2014 otherwise both token sets
1490
+ // match at once and the second layout paints with the first one's
1491
+ // palette. Head stylesheets are additive across a swap, so the class is
1492
+ // the only thing disambiguating them.
1493
+ //
1494
+ // Only what stx wrote is touched, per the markers emitted by
1495
+ // document-shell.ts. Diffing the whole element against the incoming
1496
+ // document would strip the color-mode boot's dark class and
1497
+ // data-reduced-motion, which exist only on the live page.
1498
+ if(doc.documentElement){
1499
+ var curRoot=document.documentElement,incRoot=doc.documentElement;
1500
+ var tokens=function(el,attr){var v=el.getAttribute(attr);return v?v.split(/\\s+/).filter(Boolean):[]};
1501
+ var prevCls=tokens(curRoot,'data-stx-html-class'),nextCls=tokens(incRoot,'data-stx-html-class');
1502
+ prevCls.forEach(function(c){if(nextCls.indexOf(c)===-1)curRoot.classList.remove(c)});
1503
+ nextCls.forEach(function(c){curRoot.classList.add(c)});
1504
+ if(nextCls.length)curRoot.setAttribute('data-stx-html-class',nextCls.join(' '));
1505
+ else curRoot.removeAttribute('data-stx-html-class');
1506
+
1507
+ var prevNames=tokens(curRoot,'data-stx-html-attrs'),nextNames=tokens(incRoot,'data-stx-html-attrs');
1508
+ prevNames.forEach(function(n){if(nextNames.indexOf(n)===-1)curRoot.removeAttribute(n)});
1509
+ nextNames.forEach(function(n){var v=incRoot.getAttribute(n);if(v!==null)curRoot.setAttribute(n,v)});
1510
+ if(nextNames.length)curRoot.setAttribute('data-stx-html-attrs',nextNames.join(' '));
1511
+ else curRoot.removeAttribute('data-stx-html-attrs');
1512
+ }
1513
+
1434
1514
  window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
1435
1515
 
1436
1516
  // Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
@@ -1460,7 +1540,7 @@ else {
1460
1540
  // Skip the wrap for module scripts \u2014 they get their own scope from
1461
1541
  // ESM, and top-level 'import' is illegal inside a block, which
1462
1542
  // would throw SyntaxError before the script ever runs.
1463
- var alreadyScoped=text.trimStart().charAt(0)==='('||text.trimStart().charAt(0)===';'||text.indexOf('window.stx.mount')>-1;
1543
+ var alreadyScoped=runsAlways(typeof entry==='string'?'':entry.run,text);
1464
1544
  ns.textContent=(hasImport||alreadyScoped)?text:'{'+text+'}';
1465
1545
  ns.setAttribute('data-stx-page','');
1466
1546
  var placeholder=entry.slot?document.querySelector('script[data-stx-route-script="'+entry.slot+'"]'):null;
package/dist/serve.d.ts CHANGED
@@ -65,6 +65,7 @@ export declare function render404Page(opts: {
65
65
  routes?: string[]
66
66
  isProduction: boolean
67
67
  }): string;
68
+ export declare function resolveServeOpenUrl(port: number, openPath?: string): string;
68
69
  export declare function isRenderableCacheCandidate(html: string): boolean;
69
70
  export declare function bundleBrowserAsset(entrypoint: string): Promise<Response>;
70
71
  /**
@@ -112,6 +113,7 @@ export declare interface ServeRequestContext {
112
113
  export declare interface ServeOptions {
113
114
  patterns: string[]
114
115
  port?: number
116
+ openPath?: string
115
117
  componentsDir?: string
116
118
  layoutsDir?: string
117
119
  partialsDir?: string
package/dist/serve.js CHANGED
@@ -8718,7 +8718,7 @@ var defaultConfigDir4 = /* @__PURE__ */ resolve12(process20.cwd(), "config");
8718
8718
  var defaultGeneratedDir4 = /* @__PURE__ */ resolve12(process20.cwd(), "src/generated");
8719
8719
 
8720
8720
  // src/serve.ts
8721
- import { extractPageResponseStatus, stateDir, stateDirName } from "@stacksjs/stx";
8721
+ import { BUILD_ID_HEADER, extractPageResponseStatus, getBuildId, stateDir, stateDirName } from "@stacksjs/stx";
8722
8722
  var defaultStxModule = import("@stacksjs/stx");
8723
8723
  var REACTIVE_TEMPLATE_DIRECTIVE_RE = /@for|:for|@if|:if|@else|:else|x-for|x-if|x-else/;
8724
8724
  function extractServerScriptsFromTemplate(content) {
@@ -8878,6 +8878,15 @@ function render404Page(opts) {
8878
8878
  </body>
8879
8879
  </html>`;
8880
8880
  }
8881
+ function resolveServeOpenUrl(port, openPath = "/") {
8882
+ const baseUrl = `http://localhost:${port}`;
8883
+ try {
8884
+ const url = new URL(openPath || "/", `${baseUrl}/`);
8885
+ return url.origin === baseUrl ? url.toString() : `${baseUrl}/`;
8886
+ } catch {
8887
+ return `${baseUrl}/`;
8888
+ }
8889
+ }
8881
8890
  function isRenderableCacheCandidate(html) {
8882
8891
  return !html.includes("<!-- Template Processing failed:") && !html.includes("<!-- stx rendering error -->");
8883
8892
  }
@@ -9362,7 +9371,7 @@ After installing: bun add bun-plugin-stx`);
9362
9371
  pageMiddlewarePatterns.length = 0;
9363
9372
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9364
9373
  try {
9365
- const { Router } = await import("./chunk-8nbccnre.js");
9374
+ const { Router } = await import("./chunk-3dygdr9g.js");
9366
9375
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9367
9376
  const router = new Router(process19.cwd(), { pagesDirs, stateDir: stateDirName() });
9368
9377
  if (!options.quiet)
@@ -10322,6 +10331,7 @@ ${trailingPageScripts.join(`
10322
10331
  "X-STX-Layout": pageLayout,
10323
10332
  "X-STX-Layout-Group": pageLayoutGroup,
10324
10333
  ...pageTitle && { "X-STX-Title": encodeURIComponent(pageTitle) },
10334
+ [BUILD_ID_HEADER]: getBuildId(),
10325
10335
  ...containerAttrs && { "X-STX-Container-Attrs": encodeURIComponent(containerAttrs) },
10326
10336
  "Cache-Control": "no-store",
10327
10337
  ...corsHeaders
@@ -10538,10 +10548,11 @@ ${trailingPageScripts.join(`
10538
10548
  const elapsed = (performance.now() - startTime).toFixed(0);
10539
10549
  const routeCount = sourceFiles?.length || 0;
10540
10550
  const patternsStr = patterns.join(", ");
10551
+ const openUrl = resolveServeOpenUrl(actualPort, options.openPath);
10541
10552
  console.log();
10542
10553
  console.log(` \x1B[36m\x1B[1mstx\x1B[0m`);
10543
10554
  console.log();
10544
- console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mLocal\x1B[0m: \x1B[36mhttp://localhost:${actualPort}/\x1B[0m`);
10555
+ console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mLocal\x1B[0m: \x1B[36m${openUrl}\x1B[0m`);
10545
10556
  console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mRoutes\x1B[0m: \x1B[2m${routeCount} files from ${patternsStr}\x1B[0m`);
10546
10557
  console.log();
10547
10558
  console.log(` \x1B[2mready in ${elapsed}ms\x1B[0m`);
@@ -10557,7 +10568,7 @@ ${trailingPageScripts.join(`
10557
10568
  const cmd = line.trim().toLowerCase();
10558
10569
  if (cmd === "o") {
10559
10570
  const openCmd = process19.platform === "darwin" ? "open" : process19.platform === "win32" ? "start" : "xdg-open";
10560
- Bun.spawn([openCmd, `http://localhost:${actualPort}/`]);
10571
+ Bun.spawn([openCmd, openUrl]);
10561
10572
  } else if (cmd === "q") {
10562
10573
  process19.exit(0);
10563
10574
  }
@@ -10582,6 +10593,7 @@ if (import.meta.main) {
10582
10593
  export {
10583
10594
  staticCacheControl,
10584
10595
  serve,
10596
+ resolveServeOpenUrl,
10585
10597
  render404Page,
10586
10598
  isRenderableCacheCandidate,
10587
10599
  isProductionServe,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.152",
4
+ "version": "0.2.153",
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",
@@ -39,6 +39,7 @@
39
39
  "import": "./dist/preload.js"
40
40
  },
41
41
  "./serve": {
42
+ "types": "./dist/serve.d.ts",
42
43
  "bun": "./dist/serve.js",
43
44
  "import": "./dist/serve.js"
44
45
  },