bun-plugin-stx 0.2.113 → 0.2.114

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.
@@ -597,6 +597,7 @@ function getRouterScript() {
597
597
  try{
598
598
  var transition=document.startViewTransition(callback);
599
599
  var onAbort=function(err){log('[router] view transition aborted:',err&&err.message?err.message:err)};
600
+ if(transition&&transition.ready&&transition.ready.catch)transition.ready.catch(onAbort);
600
601
  if(transition&&transition.finished&&transition.finished.catch)transition.finished.catch(onAbort);
601
602
  if(transition&&transition.updateCallbackDone&&transition.updateCallbackDone.catch)transition.updateCallbackDone.catch(onAbort);
602
603
  return true;
@@ -790,6 +791,18 @@ function getRouterScript() {
790
791
  var h=0;for(var i=0;i<code.length;i++){h=((h<<5)-h)+code.charCodeAt(i);h|=0}
791
792
  return h;
792
793
  }
794
+ function hasStaticImport(code){
795
+ return /(?:^|\\n)[ \\t]*import(?:[ \\t]+|[{*'"])/.test(code);
796
+ }
797
+ function generatedSetupName(code){
798
+ var match=code.match(/function (__stx_setup_[A-Za-z0-9_]+)\\s*\\(/);
799
+ return match?match[1]:'';
800
+ }
801
+ function isSignalsRuntimeScript(script,code){
802
+ return !!(script&&script.hasAttribute&&script.hasAttribute('data-stx-runtime'))
803
+ ||(code.indexOf('_cleanupContainer')!==-1&&code.indexOf('signals runtime loading')!==-1)
804
+ ||code.indexOf("'use strict';var cloakStyle")!==-1;
805
+ }
793
806
  // Record scripts from the initial page load
794
807
  document.querySelectorAll('script').forEach(function(s){
795
808
  var text=s.textContent||'';
@@ -981,10 +994,15 @@ else {
981
994
  function doFragSwap(){
982
995
  // Extract scripts from fragment before injecting HTML
983
996
  var fragScripts=[];
997
+ var fragScriptId=0;
984
998
  var fragStyles=[];
985
999
  var fragCrosswindCSS=null;
986
- var cleanFrag=html.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>([\\\\s\\\\S]*?)<\\\\/scr'+'ipt>','gi'),function(m,code){
987
- if(code&&code.trim())fragScripts.push(code);
1000
+ var cleanFrag=html.replace(new RegExp('<scr'+'ipt\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/scr'+'ipt>','gi'),function(m,attrs,code){
1001
+ if(code&&code.trim()&&!isSignalsRuntimeScript({hasAttribute:function(name){return name==='data-stx-runtime'&&attrs.indexOf('data-stx-runtime')!==-1}},code)){
1002
+ var slot='fragment-'+(++fragScriptId);
1003
+ fragScripts.push({text:code,slot:slot,setupName:generatedSetupName(code)});
1004
+ return '<scr'+'ipt type="application/stx-pending" data-stx-route-script="'+slot+'"></scr'+'ipt>';
1005
+ }
988
1006
  return '';
989
1007
  });
990
1008
  cleanFrag=cleanFrag.replace(new RegExp('<sty'+'le\\\\b([^>]*)>([\\\\s\\\\S]*?)<\\\\/sty'+'le>','gi'),function(m,attrs,css){
@@ -1033,7 +1051,9 @@ else {
1033
1051
  // Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
1034
1052
  log('[router] frag scripts:', fragScripts.length);
1035
1053
  document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
1036
- fragScripts.forEach(function(code){
1054
+ fragScripts.sort(function(a,b){return (a.setupName?1:0)-(b.setupName?1:0)});
1055
+ fragScripts.forEach(function(entry){
1056
+ var code=entry.text;
1037
1057
  log('[router] exec script len:', code.length, 'has __stx_setup:', code.indexOf('__stx_setup')>-1);
1038
1058
  // Skip scripts that were already executed (layout-level partials
1039
1059
  // like theme.stx, stores.stx, nav.stx). Their top-level const/let
@@ -1059,13 +1079,19 @@ else {
1059
1079
  }
1060
1080
  executedScriptHashes[h]=1;
1061
1081
  var ns=document.createElement('script');
1062
- var hasImport=code.indexOf('import ')!==-1;
1082
+ var hasImport=hasStaticImport(code);
1063
1083
  if(hasImport){
1064
1084
  ns.type='module';
1065
1085
  }
1066
1086
  ns.textContent=(hasImport||isAlreadyScoped)?code:'{'+code+'}';
1067
1087
  ns.setAttribute('data-stx-page','');
1068
- document.body.appendChild(ns);
1088
+ var placeholder=document.querySelector('script[data-stx-route-script="'+entry.slot+'"]');
1089
+ if(placeholder&&placeholder.parentNode){
1090
+ ns.setAttribute('data-stx-positioned','');
1091
+ placeholder.parentNode.replaceChild(ns,placeholder);
1092
+ }else{
1093
+ document.body.appendChild(ns);
1094
+ }
1069
1095
  });
1070
1096
  log('[router] scripts done. _latestSetup:', !!window.stx._latestSetup);
1071
1097
  // THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
@@ -1174,10 +1200,35 @@ else {
1174
1200
  var newGroup=getDocLayoutGroup(doc,newLayout);
1175
1201
  isLayoutChange=curGroup!==newGroup||(curLayout&&newLayout&&curLayout!==newLayout);
1176
1202
  }
1203
+ var incomingSetupName=newBody?(newBody.getAttribute('data-stx')||''):'';
1204
+ // Scripts assigned through innerHTML are inert. Preserve executable
1205
+ // scripts as placeholders so re-execution keeps document.currentScript
1206
+ // beside the component template it owns.
1207
+ var routedBodyScripts=[];
1208
+ var routedBodyScriptId=0;
1209
+ function prepareRoutedBodyScripts(root){
1210
+ if(!root)return;
1211
+ root.querySelectorAll('script').forEach(function(s){
1212
+ var text=s.textContent||'';
1213
+ if(s.hasAttribute('src')){s.remove();return}
1214
+ var type=(s.getAttribute('type')||'').trim().toLowerCase();
1215
+ var executable=!type||type==='text/javascript'||type==='application/javascript'||type==='module';
1216
+ if(!executable||!text.trim())return;
1217
+ if(isSignalsRuntimeScript(s,text)||text.indexOf('__stxRouter')!==-1){s.remove();return}
1218
+ var slot='document-'+(++routedBodyScriptId);
1219
+ var setupName=generatedSetupName(text);
1220
+ if(setupName)incomingSetupName=setupName;
1221
+ routedBodyScripts.push({text:text,runAlways:true,slot:slot,setupName:setupName});
1222
+ s.textContent='';
1223
+ s.setAttribute('type','application/stx-pending');
1224
+ s.setAttribute('data-stx-route-script',slot);
1225
+ });
1226
+ }
1227
+ prepareRoutedBodyScripts(isLayoutChange?newBody:newContent);
1177
1228
  if(isLayoutChange&&newBody){
1178
1229
  log('[router] full body swap for layout change');
1179
1230
  // Replace entire body content \u2014 layout chrome and all
1180
- var bodyHTML=newBody.innerHTML.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt\\\\s*>','gi'),'');
1231
+ var bodyHTML=newBody.innerHTML;
1181
1232
  document.body.innerHTML=bodyHTML;
1182
1233
  // Copy body attributes (class, data-stx, etc.)
1183
1234
  Array.from(newBody.attributes).forEach(function(attr){document.body.setAttribute(attr.name,attr.value)});
@@ -1199,8 +1250,9 @@ else {
1199
1250
  var attrMap={};
1200
1251
  Array.prototype.forEach.call(newContent.attributes,function(a){attrMap[a.name]=a.value});
1201
1252
  setContainerAttrs(currentContent,attrMap);
1202
- var cleanHTML=newContent.innerHTML.replace(new RegExp('<scr'+'ipt\\\\b[^>]*>[\\\\s\\\\S]*?<\\\\/scr'+'ipt\\\\s*>','gi'),'');
1253
+ var cleanHTML=newContent.innerHTML;
1203
1254
  currentContent.innerHTML=cleanHTML;
1255
+ if(incomingSetupName)document.body.setAttribute('data-stx',incomingSetupName);
1204
1256
  }
1205
1257
 
1206
1258
  // \u2500\u2500 Load new external <head> scripts \u2500\u2500
@@ -1226,40 +1278,20 @@ else {
1226
1278
  // Remove previously injected page scripts
1227
1279
  document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
1228
1280
 
1229
- var scripts=[];
1230
- function addScript(text, runAlways){
1231
- scripts.push({text:text,runAlways:!!runAlways});
1281
+ var scripts=routedBodyScripts.slice();
1282
+ function addScript(text, runAlways, slot){
1283
+ scripts.push({text:text,runAlways:!!runAlways,slot:slot||''});
1232
1284
  }
1233
1285
  if(isLayoutChange){
1234
- // Layout change: collect ALL scripts from the new document body
1235
- // (layout scripts, component mounts, setup functions \u2014 everything)
1236
- var newBodyEl=doc.querySelector('body');
1237
- if(newBodyEl){
1238
- newBodyEl.querySelectorAll('script').forEach(function(s){
1239
- var text=s.textContent||'';
1240
- if(s.hasAttribute('src'))return;
1241
- if(!text.trim())return;
1242
- // Skip the signals runtime IIFE \u2014 it's already loaded
1243
- if(text.indexOf("'use strict';var cloakStyle")!==-1)return;
1244
- if(text.indexOf('__stxRouter')!==-1)return;
1245
- addScript(text,true);
1246
- });
1247
- }
1248
1286
  // Also collect setup functions from <head>
1249
1287
  doc.querySelectorAll('head script').forEach(function(s){
1250
1288
  var text=s.textContent||'';
1251
1289
  if(s.hasAttribute('src'))return;
1252
1290
  if(!text.trim())return;
1291
+ if(isSignalsRuntimeScript(s,text))return;
1253
1292
  if(text.indexOf('__stx_setup_')!==-1)addScript(text,true);
1254
1293
  });
1255
1294
  } else {
1256
- // Same layout: collect scripts from the container
1257
- newContent.querySelectorAll('script').forEach(function(s){
1258
- var text=s.textContent||'';
1259
- if(s.hasAttribute('src'))return;
1260
- if(!text.trim())return;
1261
- addScript(text,true);
1262
- });
1263
1295
  // Collect page scripts from <head> AND <body> (outside container).
1264
1296
  // SSG builds place the setup function in <body> before <main>, not
1265
1297
  // in <head>. Without this, SPA navigation on static sites never
@@ -1271,16 +1303,26 @@ else {
1271
1303
  if(s.hasAttribute('src'))return;
1272
1304
  if(!text.trim())return;
1273
1305
  // Skip signals runtime and router (they contain __stx_setup references but aren't setup functions)
1274
- if(text.indexOf("'use strict';var cloakStyle")!==-1)return;
1306
+ if(isSignalsRuntimeScript(s,text))return;
1275
1307
  if(text.indexOf('__stxRouter')!==-1)return;
1276
1308
  if(newContent.contains(s))return;
1277
- if(text.indexOf('__stx_setup_')===-1&&!s.hasAttribute('data-stx-scoped')&&!s.hasAttribute('data-stx-page'))return;
1309
+ if(text.indexOf('__stx_setup_')===-1&&!s.hasAttribute('data-stx-scoped')&&!s.hasAttribute('data-stx-page')&&!s.hasAttribute('data-stx-route-params'))return;
1278
1310
  var key=text.substring(0,80);
1279
1311
  if(seenSetups[key])return;
1280
1312
  seenSetups[key]=1;
1281
1313
  addScript(text,true);
1282
1314
  });
1283
1315
  }
1316
+ // Component setup scripts also assign window.stx._latestSetup. Run the
1317
+ // destination page setup last so stx:load hydrates with the page scope,
1318
+ // not whichever nested component happened to render last.
1319
+ if(incomingSetupName){
1320
+ scripts.sort(function(a,b){
1321
+ var aPage=(a.setupName||generatedSetupName(a.text))===incomingSetupName?1:0;
1322
+ var bPage=(b.setupName||generatedSetupName(b.text))===incomingSetupName?1:0;
1323
+ return aPage-bPage;
1324
+ });
1325
+ }
1284
1326
 
1285
1327
  // Push history state (before active link updates so location.pathname is current)
1286
1328
  if(pushState!==false)history.pushState({},'',url+(hash||''));
@@ -1319,7 +1361,7 @@ else {
1319
1361
  // need special handling.
1320
1362
  var h=hashScript(text);
1321
1363
  var isSetup=text.indexOf('__stx_setup_')!==-1;
1322
- var hasImport=text.indexOf('import ')!==-1;
1364
+ var hasImport=hasStaticImport(text);
1323
1365
  if(!runAlways&&!isSetup&&executedScriptHashes[h])return;
1324
1366
  executedScriptHashes[h]=1;
1325
1367
  // Wrap scripts with import statements as modules (Bug 3 fix)
@@ -1336,7 +1378,13 @@ else {
1336
1378
  var alreadyScoped=text.trimStart().charAt(0)==='('||text.trimStart().charAt(0)===';'||text.indexOf('window.stx.mount')>-1;
1337
1379
  ns.textContent=(hasImport||alreadyScoped)?text:'{'+text+'}';
1338
1380
  ns.setAttribute('data-stx-page','');
1339
- document.body.appendChild(ns);
1381
+ var placeholder=entry.slot?document.querySelector('script[data-stx-route-script="'+entry.slot+'"]'):null;
1382
+ if(placeholder&&placeholder.parentNode){
1383
+ ns.setAttribute('data-stx-positioned','');
1384
+ placeholder.parentNode.replaceChild(ns,placeholder);
1385
+ }else{
1386
+ document.body.appendChild(ns);
1387
+ }
1340
1388
  });
1341
1389
  // THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
1342
1390
  window.dispatchEvent(new Event('stx:load'));
package/dist/serve.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Extract real top-level `<script server>` elements without matching tag-like
3
+ * text inside HTML comments or another script body. A regex over the whole
4
+ * template mistakes documentation such as `// moved from <script server>` in
5
+ * a client script for a nested server block, then removes the rest of that
6
+ * client script through its closing tag.
7
+ */
8
+ export declare function extractServerScriptsFromTemplate(content: string): {
9
+ serverScripts: string[]
10
+ templateContent: string
11
+ };
1
12
  export declare function buildDynamicRouteRegexes(fileRouteBase: string): RegExp[];
2
13
  /**
3
14
  * Are we serving in production? In production the built-in 404 must not
package/dist/serve.js CHANGED
@@ -8703,6 +8703,41 @@ var defaultGeneratedDir4 = resolve12(process20.cwd(), "src/generated");
8703
8703
  import { stateDir, stateDirName } from "@stacksjs/stx";
8704
8704
  var defaultStxModule = import("@stacksjs/stx");
8705
8705
  var REACTIVE_TEMPLATE_DIRECTIVE_RE = /@for|:for|@if|:if|@else|:else|x-for|x-if|x-else/;
8706
+ function extractServerScriptsFromTemplate(content) {
8707
+ const openRe = /<script\b([^>]*)>/gi;
8708
+ const lower = content.toLowerCase();
8709
+ const serverScripts = [];
8710
+ const retained = [];
8711
+ let retainedFrom = 0;
8712
+ let cursor = 0;
8713
+ while (cursor < content.length) {
8714
+ openRe.lastIndex = cursor;
8715
+ const match = openRe.exec(content);
8716
+ if (!match)
8717
+ break;
8718
+ const commentStart = content.indexOf("<!--", cursor);
8719
+ if (commentStart !== -1 && commentStart < match.index) {
8720
+ const commentEnd = content.indexOf("-->", commentStart + 4);
8721
+ if (commentEnd === -1)
8722
+ break;
8723
+ cursor = commentEnd + 3;
8724
+ continue;
8725
+ }
8726
+ const bodyStart = match.index + match[0].length;
8727
+ const closeStart = lower.indexOf("</script>", bodyStart);
8728
+ if (closeStart === -1)
8729
+ break;
8730
+ const end = closeStart + "</script>".length;
8731
+ if (/\bserver\b/i.test(match[1])) {
8732
+ retained.push(content.slice(retainedFrom, match.index));
8733
+ serverScripts.push(content.slice(bodyStart, closeStart));
8734
+ retainedFrom = end;
8735
+ }
8736
+ cursor = end;
8737
+ }
8738
+ retained.push(content.slice(retainedFrom));
8739
+ return { serverScripts, templateContent: retained.join("") };
8740
+ }
8706
8741
  function buildDynamicRouteRegexes(fileRouteBase) {
8707
8742
  const toPattern = (p) => p.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
8708
8743
  const patterns = [new RegExp(`^${toPattern(fileRouteBase)}$`)];
@@ -9290,7 +9325,7 @@ After installing: bun add bun-plugin-stx`);
9290
9325
  pageMiddlewarePatterns.length = 0;
9291
9326
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9292
9327
  try {
9293
- const { Router } = await import("./chunk-atra2pxg.js");
9328
+ const { Router } = await import("./chunk-gvegpv18.js");
9294
9329
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9295
9330
  const router = new Router(process19.cwd(), { pagesDirs, stateDir: stateDirName() });
9296
9331
  if (!options.quiet)
@@ -9485,13 +9520,7 @@ After installing: bun add bun-plugin-stx`);
9485
9520
  if (cachedEntry && await templateSignatureFresh(cachedEntry.signature))
9486
9521
  return cachedEntry.html;
9487
9522
  }
9488
- const serverScriptRegex = /<script\b([^>]*)\bserver\b[^>]*>([\s\S]*?)<\/script>/gi;
9489
- const serverScripts = [];
9490
- let scriptMatch;
9491
- while ((scriptMatch = serverScriptRegex.exec(content)) !== null) {
9492
- serverScripts.push(scriptMatch[2]);
9493
- }
9494
- let templateContent = content.replace(/<script\b[^>]*\bserver\b[^>]*>[\s\S]*?<\/script>/gi, "");
9523
+ const { serverScripts, templateContent } = extractServerScriptsFromTemplate(content);
9495
9524
  const context = {
9496
9525
  __filename: filePath,
9497
9526
  __dirname: nodePath.dirname(filePath)
@@ -9652,13 +9681,7 @@ After installing: bun add bun-plugin-stx`);
9652
9681
  }
9653
9682
  async function processTemplateDynamic(filePath, paramNames, paramValues, routePath, reqCtx) {
9654
9683
  const content = await Bun.file(filePath).text();
9655
- const serverScriptRegex = /<script\b([^>]*)\bserver\b[^>]*>([\s\S]*?)<\/script>/gi;
9656
- const dynServerScripts = [];
9657
- let dynScriptMatch;
9658
- while ((dynScriptMatch = serverScriptRegex.exec(content)) !== null) {
9659
- dynServerScripts.push(dynScriptMatch[2]);
9660
- }
9661
- const templateContent = content.replace(/<script\b[^>]*\bserver\b[^>]*>[\s\S]*?<\/script>/gi, "");
9684
+ const { serverScripts: dynServerScripts, templateContent } = extractServerScriptsFromTemplate(content);
9662
9685
  const context = {
9663
9686
  __filename: filePath,
9664
9687
  __dirname: nodePath.dirname(filePath),
@@ -9711,7 +9734,8 @@ After installing: bun add bun-plugin-stx`);
9711
9734
  const dependencies = new Set;
9712
9735
  output = await processDirectives(output, context, filePath, config5, dependencies);
9713
9736
  if (paramNames.length > 0) {
9714
- const paramsScript = `<script>if(window.stx)window.stx._rp=${JSON.stringify(paramsObj)};else window.__stx_rp=${JSON.stringify(paramsObj)};</script>`;
9737
+ const serializedParams = JSON.stringify(paramsObj).replace(/</g, "\\u003C").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
9738
+ const paramsScript = `<script data-stx-route-params>(function(){var p=${serializedParams};window.__stx_rp=p;if(window.stx){window.stx._rp=p;if(window.stx.setRouteParams)window.stx.setRouteParams(p)}})()</script>`;
9715
9739
  if (output.includes("</head>")) {
9716
9740
  output = output.replace("</head>", `${paramsScript}
9717
9741
  </head>`);
@@ -10164,8 +10188,12 @@ ${html}`;
10164
10188
  fragment = fragment.slice(mainStart, mainCloseIdx).trim();
10165
10189
  }
10166
10190
  const pageSetupScripts = [];
10167
- const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
10191
+ const routeParamsRe = /<script\b[^>]*data-stx-route-params[^>]*>[\s\S]*?<\/script>/gi;
10168
10192
  let setupMatch;
10193
+ while ((setupMatch = routeParamsRe.exec(content)) !== null) {
10194
+ pageSetupScripts.push(setupMatch[0]);
10195
+ }
10196
+ const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
10169
10197
  while ((setupMatch = allScriptRe.exec(content)) !== null) {
10170
10198
  const scriptContent = setupMatch[0];
10171
10199
  if (scriptContent.includes("__stx_early_mounts"))
@@ -10437,6 +10465,7 @@ export {
10437
10465
  serve,
10438
10466
  render404Page,
10439
10467
  isProductionServe,
10468
+ extractServerScriptsFromTemplate,
10440
10469
  bundleBrowserAsset,
10441
10470
  buildDynamicRouteRegexes
10442
10471
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.113",
4
+ "version": "0.2.114",
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",