bun-plugin-stx 0.2.67 → 0.2.70

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.
@@ -583,10 +583,11 @@ function getRouterScript() {
583
583
  return `
584
584
  ;(function(){
585
585
  'use strict';
586
- if(window.__stxRouter)return;
586
+ var ROUTER_REV=3;
587
+ if(window.__stxRouter&&window.__stxRouter.__rev===ROUTER_REV)return;
587
588
 
588
589
  // \u2500\u2500 Configuration \u2500\u2500
589
- var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true,progress:true,progressColor:'#78dce8',progressHeight:'2px',interceptAllLinks:false};
590
+ 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};
590
591
  var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
591
592
  var containerSel=o.container;
592
593
  var debug=!!o.debug;
@@ -646,6 +647,40 @@ function getRouterScript() {
646
647
  var prefetching={};
647
648
  var isNavigating=false;
648
649
 
650
+ // LRU bookkeeping for cache/layoutCache/layoutGroupCache. Pre-fix
651
+ // (stacksjs/stx#1719) these were plain objects with no eviction, so
652
+ // hover-prefetch on a sidebar of 200 internal links retained ~10MB
653
+ // of HTML strings for the page's lifetime. cacheOrder is a most-
654
+ // recently-used-last list of keys; setCache pushes new entries and
655
+ // evicts the oldest when length exceeds o.prefetchCacheMax (default
656
+ // 50). touchCache promotes a key on cache-hit. We use a plain array
657
+ // rather than a Map to keep the diff localized and the runtime
658
+ // hand-minified shape consistent.
659
+ var cacheOrder=[];
660
+ function setCache(key,html,layout,group){
661
+ if(!(key in cache))cacheOrder.push(key);
662
+ cache[key]=html;
663
+ layoutCache[key]=layout;
664
+ layoutGroupCache[key]=group;
665
+ while(cacheOrder.length>o.prefetchCacheMax){
666
+ var oldest=cacheOrder.shift();
667
+ delete cache[oldest];
668
+ delete layoutCache[oldest];
669
+ delete layoutGroupCache[oldest];
670
+ }
671
+ }
672
+ function touchCache(key){
673
+ var i=cacheOrder.indexOf(key);
674
+ if(i>=0){cacheOrder.splice(i,1);cacheOrder.push(key)}
675
+ }
676
+ function evictCache(key){
677
+ var i=cacheOrder.indexOf(key);
678
+ if(i>=0)cacheOrder.splice(i,1);
679
+ delete cache[key];
680
+ delete layoutCache[key];
681
+ delete layoutGroupCache[key];
682
+ }
683
+
649
684
  // Extract top-level CSS blocks (rules AND @media blocks with nested braces).
650
685
  // The old regex ([^{}]+{[^{}]*}) silently dropped @media blocks because
651
686
  // it couldn't handle nested braces \u2014 causing responsive classes (lg:, md:)
@@ -713,6 +748,12 @@ function getRouterScript() {
713
748
  var u=new URL(url,location.origin);
714
749
  return u.pathname+u.search;
715
750
  }
751
+ // Keep SPA fetches inside the active locale (/en/... when viewing English).
752
+ function withCurrentLocale(href){
753
+ var d=window.__stxI18n;
754
+ if(d&&typeof d.localizeHref==='function')return d.localizeHref(href);
755
+ return href;
756
+ }
716
757
  function defaultLayoutGroup(layout){
717
758
  if(!layout)return 'app';
718
759
  var clean=String(layout).replace(/\\\\/g,'/');
@@ -756,6 +797,9 @@ function getRouterScript() {
756
797
  }
757
798
 
758
799
  function navigate(url,pushState,force){
800
+ // Lang-picker passes force=true with an already-localized path (/en/...).
801
+ // Re-localizing would map it back to the *current* locale and no-op.
802
+ if(!force) url=withCurrentLocale(url);
759
803
  log('[router] navigate() called:',url,'isNavigating:',isNavigating);
760
804
  if(isNavigating)return;
761
805
  var t=new URL(url,location.origin);
@@ -788,6 +832,9 @@ function getRouterScript() {
788
832
  function done(){isNavigating=false;document.body.classList.remove(o.loadingClass);finishProgress()}
789
833
 
790
834
  if(o.cache&&cache[targetPath]&&!force){
835
+ // Cache hit \u2192 promote in LRU order so this entry survives eviction
836
+ // longer than other entries that haven't been touched.
837
+ touchCache(targetPath);
791
838
  if(checkLayoutChange(layoutCache[targetPath],url,layoutGroupCache[targetPath])){
792
839
  // Layout changed \u2014 fetch full page and do full document swap
793
840
  log('[router] cache hit but layout changed \u2014 fetching full page');
@@ -807,7 +854,7 @@ function getRouterScript() {
807
854
  done();
808
855
  }
809
856
  else {
810
- if(force&&cache[targetPath])delete cache[targetPath];
857
+ if(force&&cache[targetPath])evictCache(targetPath);
811
858
  // First fetch as fragment (SPA mode) when the configured container is
812
859
  // the page content. Custom app-shell containers need full documents so
813
860
  // the router does not inject a <main> fragment into the wrong element.
@@ -833,7 +880,7 @@ else {
833
880
  }).then(function(result){
834
881
  if(!result)return;
835
882
  if(result.isFragment)result.html='<!--stx-fragment-->'+result.html;
836
- if(o.cache){cache[targetPath]=result.html;layoutCache[targetPath]=result.layout;layoutGroupCache[targetPath]=result.layoutGroup}
883
+ if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup);
837
884
  swap(result.html,targetPath,pushState,targetHash);
838
885
  }).catch(function(err){
839
886
  console.error('[router] fetch error:',err);
@@ -1207,6 +1254,7 @@ else {
1207
1254
  if(href.startsWith('http')||href.startsWith('#')||href.startsWith('mailto:')||href.startsWith('tel:')||href.startsWith('javascript:'))return false;
1208
1255
  if(link.target==='_blank')return false;
1209
1256
  if(link.hasAttribute('data-stx-no-router')||link.hasAttribute('data-no-router')||link.hasAttribute('download'))return false;
1257
+ if(href.startsWith('?'))return true;
1210
1258
  if(href===location.pathname)return false;
1211
1259
  if(!getContainer())return false;
1212
1260
  return true;
@@ -1233,7 +1281,7 @@ else {
1233
1281
  e.preventDefault();
1234
1282
  e.stopPropagation();
1235
1283
  log('[router] navigating to:',href);
1236
- navigate(href);
1284
+ navigate(withCurrentLocale(href));
1237
1285
  },true);
1238
1286
 
1239
1287
  // \u2500\u2500 Back/forward \u2500\u2500
@@ -1247,7 +1295,7 @@ else {
1247
1295
  if(!e.target||!e.target.closest)return;
1248
1296
  var link=e.target.closest('[data-stx-link]');
1249
1297
  if(!link)return;
1250
- var href=link.getAttribute('href');
1298
+ var href=withCurrentLocale(link.getAttribute('href'));
1251
1299
  var key=cacheKey(href);
1252
1300
  if(cache[key]||prefetching[key])return;
1253
1301
  prefetching[key]=true;
@@ -1258,7 +1306,7 @@ else {
1258
1306
  var pGroup=r.headers.get('X-STX-Layout-Group')||'';
1259
1307
  return r.text().then(function(html){return{html:isFrag?'<!--stx-fragment-->'+html:html,layout:pLayout,layoutGroup:pGroup}});
1260
1308
  }).then(function(result){
1261
- if(o.cache){cache[key]=result.html;layoutCache[key]=result.layout;layoutGroupCache[key]=result.layoutGroup}
1309
+ if(o.cache)setCache(key,result.html,result.layout,result.layoutGroup);
1262
1310
  }).catch(function(){}).finally(function(){delete prefetching[key]});
1263
1311
  },true);
1264
1312
  }
@@ -1355,15 +1403,16 @@ else {
1355
1403
  var key=cacheKey(url);
1356
1404
  if(!cache[key]){
1357
1405
  var wantsFragment=shouldUseFragmentResponse();
1358
- 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')||'';return r.text().then(function(html){return{html:isFrag?'<!--stx-fragment-->'+html:html,layout:pLayout,layoutGroup:pGroup}})}).then(function(result){cache[key]=result.html;layoutCache[key]=result.layout;layoutGroupCache[key]=result.layoutGroup}).catch(function(){});
1406
+ 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')||'';return r.text().then(function(html){return{html:isFrag?'<!--stx-fragment-->'+html:html,layout:pLayout,layoutGroup:pGroup}})}).then(function(result){setCache(key,result.html,result.layout,result.layoutGroup)}).catch(function(){});
1359
1407
  }
1360
1408
  },
1361
- 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]},
1409
+ 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},
1362
1410
  cache:cache,
1363
1411
  swap:swap,
1364
1412
  updateNav:updateNav
1365
1413
  };
1366
1414
 
1415
+ router.__rev=ROUTER_REV;
1367
1416
  window.__stxRouter=router;
1368
1417
  window.stxRouter=router;
1369
1418
  if(window.stx)window.stx.router=router;
package/dist/serve.d.ts CHANGED
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Build the candidate regexes for a dynamic route file (stacksjs/stx#1927).
3
+ *
4
+ * `fileRouteBase` is the route path with the file extension already
5
+ * stripped (e.g. `judges/[id]/profile/index`). Returns one regex per
6
+ * acceptable URL shape:
7
+ * - the literal path with `[param]` → `([^/]+)`
8
+ * - the same with a trailing `/index` removed, so
9
+ * `judges/[id]/profile/index` also matches `judges/35/profile`
10
+ * (without this, nested dynamic routes 404 on hard reload)
11
+ * - both of the above again with a leading `pages/` stripped, for
12
+ * projects that nest routable views under `pages/`
13
+ */
14
+ export declare function buildDynamicRouteRegexes(fileRouteBase: string): RegExp[];
1
15
  export declare function bundleBrowserAsset(entrypoint: string): Promise<Response>;
2
16
  /**
3
17
  * Start the STX development server
package/dist/serve.js CHANGED
@@ -7039,6 +7039,7 @@ class Logger3 {
7039
7039
  }
7040
7040
  }
7041
7041
  var logger3 = new Logger3("stacks");
7042
+
7042
7043
  class BunfigError2 extends Error {
7043
7044
  timestamp;
7044
7045
  context;
@@ -8693,6 +8694,21 @@ var defaultGeneratedDir4 = resolve12(process20.cwd(), "src/generated");
8693
8694
 
8694
8695
  // src/serve.ts
8695
8696
  var defaultStxModule = import("@stacksjs/stx");
8697
+ function buildDynamicRouteRegexes(fileRouteBase) {
8698
+ const toPattern = (p) => p.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
8699
+ const patterns = [new RegExp(`^${toPattern(fileRouteBase)}$`)];
8700
+ const noIndex = fileRouteBase.replace(/\/index$/, "");
8701
+ if (noIndex !== fileRouteBase)
8702
+ patterns.push(new RegExp(`^${toPattern(noIndex)}$`));
8703
+ if (fileRouteBase.startsWith("pages/")) {
8704
+ const pretty = fileRouteBase.slice(6);
8705
+ patterns.push(new RegExp(`^${toPattern(pretty)}$`));
8706
+ const prettyNoIndex = pretty.replace(/\/index$/, "");
8707
+ if (prettyNoIndex !== pretty)
8708
+ patterns.push(new RegExp(`^${toPattern(prettyNoIndex)}$`));
8709
+ }
8710
+ return patterns;
8711
+ }
8696
8712
  var defaultStxConfig = {
8697
8713
  partialsDir: "partials",
8698
8714
  componentsDir: "components",
@@ -8766,14 +8782,14 @@ async function bundleBrowserAsset(entrypoint) {
8766
8782
  status: 500,
8767
8783
  headers: {
8768
8784
  "Content-Type": "text/plain; charset=utf-8",
8769
- "Cache-Control": "no-cache"
8785
+ "Cache-Control": "no-store"
8770
8786
  }
8771
8787
  });
8772
8788
  }
8773
8789
  return new Response(await result.outputs[0].text(), {
8774
8790
  headers: {
8775
8791
  "Content-Type": "application/javascript",
8776
- "Cache-Control": "no-cache"
8792
+ "Cache-Control": "no-store"
8777
8793
  }
8778
8794
  });
8779
8795
  }
@@ -8819,8 +8835,10 @@ After installing: bun add bun-plugin-stx`);
8819
8835
  }
8820
8836
  const routes = new Map;
8821
8837
  let sourceFiles = null;
8838
+ let discoveredPagePaths = null;
8822
8839
  let assetsInitialized = false;
8823
8840
  const htmlCache = new Map;
8841
+ const ENABLE_HTML_CACHE = false;
8824
8842
  const crosswindCssCache = new Map;
8825
8843
  const hmrClients = new Set;
8826
8844
  const hmrEncoder = new TextEncoder;
@@ -8991,6 +9009,31 @@ After installing: bun add bun-plugin-stx`);
8991
9009
  const authRedirectTo = authConfig?.redirectTo ?? "/login";
8992
9010
  const authHome = authConfig?.home ?? "/";
8993
9011
  const extraProtectedPrefixes = authConfig?.protectedPaths ?? [];
9012
+ let activeServeLocale = null;
9013
+ let activeServeSearch = "";
9014
+ function injectServeLocaleContext(context) {
9015
+ if (!activeServeLocale)
9016
+ return;
9017
+ const loc = activeServeLocale;
9018
+ context.locale = loc;
9019
+ context.t = (key, values) => {
9020
+ const translate = globalThis.t;
9021
+ return typeof translate === "function" ? translate(key, values, loc) : key;
9022
+ };
9023
+ }
9024
+ function injectServeRequestContext(context) {
9025
+ if (!activeServeSearch)
9026
+ return;
9027
+ context.__stxServeSearch = activeServeSearch;
9028
+ globalThis.__stxServeSearch = activeServeSearch;
9029
+ }
9030
+ function htmlCacheKey(filePath) {
9031
+ const loc = i18nConfig ? activeServeLocale ?? i18nConfig.defaultLocale : "";
9032
+ const search = activeServeSearch || "";
9033
+ if (!loc && !search)
9034
+ return filePath;
9035
+ return `${filePath}\x00${loc}\x00${search}`;
9036
+ }
8994
9037
  const builtInMiddleware = authConfig === null ? {} : {
8995
9038
  auth: (_req, ctx) => {
8996
9039
  const tok = ctx.cookies[authCookieName];
@@ -9027,6 +9070,37 @@ After installing: bun add bun-plugin-stx`);
9027
9070
  return out;
9028
9071
  }
9029
9072
  const globalMiddlewareNames = options.globalMiddleware ?? [];
9073
+ function stxViewFileToRoutePath(filePath) {
9074
+ const normalized = filePath.replace(/\\/g, "/");
9075
+ for (const pattern of patterns) {
9076
+ const normPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
9077
+ if (!normalized.startsWith(`${normPattern}/`))
9078
+ continue;
9079
+ let rel = normalized.slice(normPattern.length + 1).replace(/\.(stx|md|html)$/, "");
9080
+ if (rel.includes("["))
9081
+ return null;
9082
+ if (rel === "index" || rel === "")
9083
+ return "/";
9084
+ if (rel.endsWith("/index"))
9085
+ rel = rel.slice(0, -6) || "index";
9086
+ return rel === "index" ? "/" : `/${rel}`;
9087
+ }
9088
+ return null;
9089
+ }
9090
+ function rebuildDiscoveredPagePaths(files) {
9091
+ const known = new Set(["/"]);
9092
+ for (const filePath of files) {
9093
+ const routePath = stxViewFileToRoutePath(filePath);
9094
+ if (routePath)
9095
+ known.add(routePath);
9096
+ }
9097
+ const pages = options.site?.pages;
9098
+ if (pages) {
9099
+ for (const p of Object.keys(pages))
9100
+ known.add(p.startsWith("/") ? p : `/${p}`);
9101
+ }
9102
+ discoveredPagePaths = known;
9103
+ }
9030
9104
  async function discoverFiles() {
9031
9105
  if (sourceFiles !== null)
9032
9106
  return sourceFiles;
@@ -9062,11 +9136,12 @@ After installing: bun add bun-plugin-stx`);
9062
9136
  }
9063
9137
  }
9064
9138
  sourceFiles = files;
9139
+ rebuildDiscoveredPagePaths(files);
9065
9140
  pageMiddlewareByPath.clear();
9066
9141
  pageMiddlewarePatterns.length = 0;
9067
9142
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9068
9143
  try {
9069
- const { Router } = await import("./chunk-wv4bcqba.js");
9144
+ const { Router } = await import("./chunk-sm4gbwrd.js");
9070
9145
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9071
9146
  const router = new Router(process19.cwd(), { pagesDirs });
9072
9147
  if (!options.quiet)
@@ -9235,10 +9310,14 @@ After installing: bun add bun-plugin-stx`);
9235
9310
  }
9236
9311
  }
9237
9312
  async function processTemplate(filePath) {
9238
- const cachedEntry = htmlCache.get(filePath);
9239
- if (cachedEntry && await templateSignatureFresh(cachedEntry.signature))
9240
- return cachedEntry.html;
9241
9313
  const content = await Bun.file(filePath).text();
9314
+ const skipCacheHint = /(?:^|[^\w$])__stx_skip_cache\s*=\s*true/.test(content);
9315
+ if (ENABLE_HTML_CACHE && !skipCacheHint) {
9316
+ const cacheKey = htmlCacheKey(filePath);
9317
+ const cachedEntry = htmlCache.get(cacheKey);
9318
+ if (cachedEntry && await templateSignatureFresh(cachedEntry.signature))
9319
+ return cachedEntry.html;
9320
+ }
9242
9321
  const serverScriptRegex = /<script\b([^>]*)\bserver\b[^>]*>([\s\S]*?)<\/script>/gi;
9243
9322
  const serverScripts = [];
9244
9323
  let scriptMatch;
@@ -9252,6 +9331,8 @@ After installing: bun add bun-plugin-stx`);
9252
9331
  };
9253
9332
  const { processDirectives, extractVariables, defaultConfig: defaultConfig4, generateSpaShell, injectRouterScript, resetHead } = await stxModule;
9254
9333
  resetHead();
9334
+ injectServeLocaleContext(context);
9335
+ injectServeRequestContext(context);
9255
9336
  for (const scriptBody of serverScripts) {
9256
9337
  await extractVariables(scriptBody, context, filePath);
9257
9338
  }
@@ -9284,7 +9365,7 @@ After installing: bun add bun-plugin-stx`);
9284
9365
  let output = templateContent;
9285
9366
  const dependencies = new Set;
9286
9367
  output = await processDirectives(output, context, filePath, config5, dependencies);
9287
- output = await injectRouterScript(output);
9368
+ output = await injectRouterScript(output, getRouterInjectOptions());
9288
9369
  const directiveTemplateRe = /@for|:for|@if|:if|x-for|x-if/;
9289
9370
  const hasDirectiveTemplates = /<template\s[^>]*(?:@for|:for|@if|:if|x-for|x-if)/.test(output);
9290
9371
  if (hasDirectiveTemplates) {
@@ -9298,9 +9379,9 @@ After installing: bun add bun-plugin-stx`);
9298
9379
  } else {
9299
9380
  output = output.replace(/<template[^>]*>/gi, "").replace(/<\/template>/gi, "");
9300
9381
  }
9301
- if (!context.__stx_skip_cache) {
9382
+ if (ENABLE_HTML_CACHE && !context.__stx_skip_cache && !skipCacheHint) {
9302
9383
  const signature = await buildTemplateSignature(filePath, dependencies);
9303
- htmlCache.set(filePath, { html: output, signature });
9384
+ htmlCache.set(htmlCacheKey(filePath), { html: output, signature });
9304
9385
  }
9305
9386
  return output;
9306
9387
  }
@@ -9384,25 +9465,7 @@ After installing: bun add bun-plugin-stx`);
9384
9465
  }
9385
9466
  }
9386
9467
  const fileRouteBase = relativeFilePath.replace(/\.(stx|md|html)$/, "");
9387
- const routePattern = fileRouteBase.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9388
- const regexPatterns = [
9389
- new RegExp(`^${routePattern}$`)
9390
- ];
9391
- const fileRouteNoIndex = fileRouteBase.replace(/\/index$/, "");
9392
- if (fileRouteNoIndex !== fileRouteBase) {
9393
- const noIndexPattern = fileRouteNoIndex.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9394
- regexPatterns.push(new RegExp(`^${noIndexPattern}$`));
9395
- }
9396
- if (fileRouteBase.startsWith("pages/")) {
9397
- const prettyBase = fileRouteBase.slice(6);
9398
- const prettyPattern = prettyBase.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9399
- regexPatterns.push(new RegExp(`^${prettyPattern}$`));
9400
- const prettyNoIndex = prettyBase.replace(/\/index$/, "");
9401
- if (prettyNoIndex !== prettyBase) {
9402
- const prettyNoIndexPattern = prettyNoIndex.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9403
- regexPatterns.push(new RegExp(`^${prettyNoIndexPattern}$`));
9404
- }
9405
- }
9468
+ const regexPatterns = buildDynamicRouteRegexes(fileRouteBase);
9406
9469
  for (const regex of regexPatterns) {
9407
9470
  const match = normalizedPath.match(regex);
9408
9471
  if (match) {
@@ -9435,6 +9498,7 @@ After installing: bun add bun-plugin-stx`);
9435
9498
  }
9436
9499
  const { processDirectives, extractVariables, defaultConfig: defaultConfig4, injectRouterScript: injectRouter, resetHead: resetHeadDyn } = await stxModule;
9437
9500
  resetHeadDyn();
9501
+ injectServeLocaleContext(context);
9438
9502
  for (const scriptBody of dynServerScripts) {
9439
9503
  await extractVariables(scriptBody, context, filePath);
9440
9504
  }
@@ -9474,7 +9538,7 @@ After installing: bun add bun-plugin-stx`);
9474
9538
  ${output}`;
9475
9539
  }
9476
9540
  }
9477
- output = await injectRouter(output);
9541
+ output = await injectRouter(output, getRouterInjectOptions());
9478
9542
  const directiveTemplateRe2 = /@for|:for|@if|:if|x-for|x-if/;
9479
9543
  output = output.replace(/<template(?:\s[^>]*)?>|<\/template>/gi, (match) => {
9480
9544
  if (directiveTemplateRe2.test(match))
@@ -9512,6 +9576,28 @@ ${output}`;
9512
9576
  }
9513
9577
  return { locale: i18nConfig.defaultLocale, path: pathname };
9514
9578
  }
9579
+ function getRouterInjectOptions() {
9580
+ return {
9581
+ router: {
9582
+ interceptAllLinks: true,
9583
+ container: "main",
9584
+ ...siteConfig?.router ?? {},
9585
+ ...stxConfig.router ?? {}
9586
+ }
9587
+ };
9588
+ }
9589
+ function ensureRouterConfig(html) {
9590
+ const routerOpts = getRouterInjectOptions().router;
9591
+ const block = `<script>window.__stxRouterConfig=${JSON.stringify(routerOpts)};</script>
9592
+ `;
9593
+ const configTagRe = /<script>window\.__stxRouterConfig=\{[^<]*\}<\/script>\s*/i;
9594
+ if (configTagRe.test(html))
9595
+ return html.replace(configTagRe, block);
9596
+ if (/<\/body>/i.test(html))
9597
+ return html.replace(/<\/body>/i, `${block}</body>`);
9598
+ return `${html}
9599
+ ${block}`;
9600
+ }
9515
9601
  async function applyI18nToResponse(res, locale, normalizedPath) {
9516
9602
  if (!siteStxPromise)
9517
9603
  return res;
@@ -9542,8 +9628,8 @@ ${output}`;
9542
9628
  html = `${groupMeta}
9543
9629
  ${html}`;
9544
9630
  }
9545
- if (i18nConfig && locale !== i18nConfig.defaultLocale && siteConfig?.pages) {
9546
- const knownPaths = new Set(Object.keys(siteConfig.pages));
9631
+ const knownPaths = discoveredPagePaths ?? new Set;
9632
+ if (i18nConfig && locale !== i18nConfig.defaultLocale && knownPaths.size > 0) {
9547
9633
  html = html.replace(/<a\b([^>]*?)\bhref="(\/[^"]*)"([^>]*)>/gi, (full, before, href, after) => {
9548
9634
  if (href.startsWith("//"))
9549
9635
  return full;
@@ -9562,6 +9648,7 @@ ${html}`;
9562
9648
  }
9563
9649
  if (i18nConfig && stx.applyTranslations)
9564
9650
  html = stx.applyTranslations(html, i18nConfig, locale);
9651
+ html = ensureRouterConfig(html);
9565
9652
  const headers = new Headers(res.headers);
9566
9653
  headers.delete("content-length");
9567
9654
  return new Response(html, {
@@ -9590,411 +9677,421 @@ ${html}`;
9590
9677
  if (stripped.path !== url.pathname) {
9591
9678
  const rewritten = new URL(req.url);
9592
9679
  rewritten.pathname = stripped.path;
9593
- req = new Request(rewritten, req);
9680
+ const headers = new Headers(req.headers);
9681
+ headers.set("x-stx-locale", stripped.locale);
9682
+ req = new Request(rewritten, { headers, method: req.method, body: req.body, redirect: req.redirect, duplex: "half" });
9594
9683
  path = stripped.path;
9684
+ } else if (i18nLocale) {
9685
+ const headers = new Headers(req.headers);
9686
+ headers.set("x-stx-locale", i18nLocale);
9687
+ req = new Request(req, { headers });
9595
9688
  }
9596
9689
  }
9597
9690
  const _i18nResp = await (async () => {
9598
- if (req.method === "OPTIONS") {
9599
- return new Response(null, { headers: corsHeaders });
9600
- }
9601
- if (path === "/_stx/hmr") {
9602
- let controller;
9603
- let keepalive;
9604
- const stream = new ReadableStream({
9605
- start(c) {
9606
- controller = c;
9607
- hmrClients.add(c);
9608
- c.enqueue(hmrEncoder.encode(`data: {"type":"connected"}
9691
+ activeServeLocale = i18nLocale;
9692
+ activeServeSearch = url.search;
9693
+ try {
9694
+ if (req.method === "OPTIONS") {
9695
+ return new Response(null, { headers: corsHeaders });
9696
+ }
9697
+ if (path === "/_stx/hmr") {
9698
+ let controller;
9699
+ let keepalive;
9700
+ const stream = new ReadableStream({
9701
+ start(c) {
9702
+ controller = c;
9703
+ hmrClients.add(c);
9704
+ c.enqueue(hmrEncoder.encode(`data: {"type":"connected"}
9609
9705
 
9610
9706
  `));
9611
- keepalive = setInterval(() => {
9612
- try {
9613
- c.enqueue(hmrEncoder.encode(`: keepalive
9707
+ keepalive = setInterval(() => {
9708
+ try {
9709
+ c.enqueue(hmrEncoder.encode(`: keepalive
9614
9710
 
9615
9711
  `));
9616
- } catch {}
9617
- }, 15000);
9618
- },
9619
- cancel() {
9620
- if (keepalive)
9621
- clearInterval(keepalive);
9622
- if (controller)
9623
- hmrClients.delete(controller);
9624
- }
9625
- });
9626
- return new Response(stream, {
9627
- headers: {
9628
- "Content-Type": "text/event-stream",
9629
- "Cache-Control": "no-cache, no-transform",
9630
- Connection: "keep-alive",
9631
- "X-Accel-Buffering": "no"
9632
- }
9633
- });
9634
- }
9635
- if (options.onRequest) {
9636
- const customResponse = await options.onRequest(req);
9637
- if (customResponse)
9638
- return customResponse;
9639
- }
9640
- const route = resolveRouteMiddleware(path);
9641
- const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
9642
- const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
9643
- if (requested.length > 0) {
9644
- const expanded = expandMiddlewareNames(requested);
9645
- const cookies = parseCookies(req);
9646
- const ctx = {
9647
- path,
9648
- url,
9649
- params: route.params,
9650
- cookies,
9651
- redirect: (to, status = 302) => {
9652
- const sep = to.includes("?") ? "&" : "?";
9653
- const next = encodeURIComponent(path + (url.search || ""));
9654
- return new Response(null, {
9655
- status,
9656
- headers: { Location: `${to}${sep}next=${next}` }
9657
- });
9658
- }
9659
- };
9660
- for (const entry of expanded) {
9661
- const colon = entry.indexOf(":");
9662
- const name = colon === -1 ? entry : entry.slice(0, colon);
9663
- const args = colon === -1 ? [] : entry.slice(colon + 1).split(",");
9664
- const handler = middlewareRegistry[name];
9665
- if (!handler) {
9666
- console.warn(`[stx serve] unknown middleware "${name}" on ${path}`);
9667
- continue;
9668
- }
9669
- const result = await handler(req, ctx, ...args);
9670
- if (result instanceof Response)
9671
- return result;
9712
+ } catch {}
9713
+ }, 15000);
9714
+ },
9715
+ cancel() {
9716
+ if (keepalive)
9717
+ clearInterval(keepalive);
9718
+ if (controller)
9719
+ hmrClients.delete(controller);
9720
+ }
9721
+ });
9722
+ return new Response(stream, {
9723
+ headers: {
9724
+ "Content-Type": "text/event-stream",
9725
+ "Cache-Control": "no-cache, no-transform",
9726
+ Connection: "keep-alive",
9727
+ "X-Accel-Buffering": "no"
9728
+ }
9729
+ });
9672
9730
  }
9673
- }
9674
- if (options.routes) {
9675
- const routeHandler = options.routes[path];
9676
- if (routeHandler) {
9677
- return routeHandler(req);
9731
+ if (options.onRequest) {
9732
+ const customResponse = await options.onRequest(req);
9733
+ if (customResponse)
9734
+ return customResponse;
9678
9735
  }
9679
- }
9680
- if (path.startsWith("/_stx/component/")) {
9681
- const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
9682
- if (componentName) {
9683
- try {
9684
- const { processDirectives, defaultConfig: defaultConfig4 } = await stxModule;
9685
- const componentTemplate = `<${componentName} />`;
9686
- const componentOpts = {
9687
- ...defaultConfig4,
9688
- ...componentsDir && { componentsDir },
9689
- ...layoutsDir && { layoutsDir },
9690
- ...partialsDir && { partialsDir },
9691
- autoShell: false
9692
- };
9693
- const html = await processDirectives(componentTemplate, {}, `${componentsDir}/${componentName}.stx`, componentOpts, new Set);
9694
- return new Response(html, {
9695
- headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9696
- });
9697
- } catch (e) {
9698
- return new Response(`<div class="stx-async-error">${e.message}</div>`, {
9699
- status: 500,
9700
- headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9701
- });
9736
+ const route = resolveRouteMiddleware(path);
9737
+ const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
9738
+ const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
9739
+ if (requested.length > 0) {
9740
+ const expanded = expandMiddlewareNames(requested);
9741
+ const cookies = parseCookies(req);
9742
+ const ctx = {
9743
+ path,
9744
+ url,
9745
+ params: route.params,
9746
+ cookies,
9747
+ redirect: (to, status = 302) => {
9748
+ const sep = to.includes("?") ? "&" : "?";
9749
+ const next = encodeURIComponent(path + (url.search || ""));
9750
+ return new Response(null, {
9751
+ status,
9752
+ headers: { Location: `${to}${sep}next=${next}` }
9753
+ });
9754
+ }
9755
+ };
9756
+ for (const entry of expanded) {
9757
+ const colon = entry.indexOf(":");
9758
+ const name = colon === -1 ? entry : entry.slice(0, colon);
9759
+ const args = colon === -1 ? [] : entry.slice(colon + 1).split(",");
9760
+ const handler = middlewareRegistry[name];
9761
+ if (!handler) {
9762
+ console.warn(`[stx serve] unknown middleware "${name}" on ${path}`);
9763
+ continue;
9764
+ }
9765
+ const result = await handler(req, ctx, ...args);
9766
+ if (result instanceof Response)
9767
+ return result;
9702
9768
  }
9703
9769
  }
9704
- }
9705
- if (path.startsWith("/api/data/") && req.method === "POST") {
9706
- try {
9707
- const tableName = path.replace("/api/data/", "").split("/")[0];
9708
- if (!tableName) {
9709
- return new Response(JSON.stringify({ error: "Table name required" }), {
9710
- status: 400,
9711
- headers: { "Content-Type": "application/json", ...corsHeaders }
9712
- });
9770
+ if (options.routes) {
9771
+ const routeHandler = options.routes[path];
9772
+ if (routeHandler) {
9773
+ return routeHandler(req);
9713
9774
  }
9714
- if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
9715
- return new Response(JSON.stringify({ error: "Invalid table name" }), {
9716
- status: 400,
9717
- headers: { "Content-Type": "application/json", ...corsHeaders }
9718
- });
9775
+ }
9776
+ if (path.startsWith("/_stx/component/")) {
9777
+ const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
9778
+ if (componentName) {
9779
+ try {
9780
+ const { processDirectives, defaultConfig: defaultConfig4 } = await stxModule;
9781
+ const componentTemplate = `<${componentName} />`;
9782
+ const componentOpts = {
9783
+ ...defaultConfig4,
9784
+ ...componentsDir && { componentsDir },
9785
+ ...layoutsDir && { layoutsDir },
9786
+ ...partialsDir && { partialsDir },
9787
+ autoShell: false
9788
+ };
9789
+ const html = await processDirectives(componentTemplate, {}, `${componentsDir}/${componentName}.stx`, componentOpts, new Set);
9790
+ return new Response(html, {
9791
+ headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9792
+ });
9793
+ } catch (e) {
9794
+ return new Response(`<div class="stx-async-error">${e.message}</div>`, {
9795
+ status: 500,
9796
+ headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9797
+ });
9798
+ }
9719
9799
  }
9720
- const body = await req.json();
9721
- const { Database } = await import("bun:sqlite");
9722
- const dbPath = nodePath.resolve(process19.cwd(), "database/stacks.sqlite");
9723
- const db = new Database(dbPath);
9800
+ }
9801
+ if (path.startsWith("/api/data/") && req.method === "POST") {
9724
9802
  try {
9725
- const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
9726
- const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
9727
- const columns = [];
9728
- const placeholders = [];
9729
- const values = [];
9730
- for (const col of validColumns) {
9731
- if (body[col] !== undefined && body[col] !== "") {
9732
- columns.push(col);
9733
- placeholders.push("?");
9734
- values.push(body[col]);
9735
- }
9736
- }
9737
- const now = new Date().toISOString();
9738
- if (tableInfo.some((c) => c.name === "created_at")) {
9739
- columns.push("created_at");
9740
- placeholders.push("?");
9741
- values.push(now);
9742
- }
9743
- if (tableInfo.some((c) => c.name === "updated_at")) {
9744
- columns.push("updated_at");
9745
- placeholders.push("?");
9746
- values.push(now);
9803
+ const tableName = path.replace("/api/data/", "").split("/")[0];
9804
+ if (!tableName) {
9805
+ return new Response(JSON.stringify({ error: "Table name required" }), {
9806
+ status: 400,
9807
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9808
+ });
9747
9809
  }
9748
- if (columns.length === 0) {
9749
- return new Response(JSON.stringify({ error: "No valid fields provided" }), {
9810
+ if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
9811
+ return new Response(JSON.stringify({ error: "Invalid table name" }), {
9750
9812
  status: 400,
9751
9813
  headers: { "Content-Type": "application/json", ...corsHeaders }
9752
9814
  });
9753
9815
  }
9754
- const query = `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
9755
- const stmt = db.prepare(query);
9756
- const result = stmt.run(...values);
9757
- return new Response(JSON.stringify({
9758
- success: true,
9759
- id: result.lastInsertRowid,
9760
- message: "Record created successfully"
9761
- }), {
9762
- status: 201,
9763
- headers: { "Content-Type": "application/json", ...corsHeaders }
9764
- });
9765
- } catch (error) {
9766
- console.error("API Error:", error);
9767
- return new Response(JSON.stringify({
9768
- error: error.message || "Failed to create record"
9769
- }), {
9770
- status: 500,
9771
- headers: { "Content-Type": "application/json", ...corsHeaders }
9772
- });
9773
- } finally {
9774
- db.close();
9775
- }
9776
- } catch {}
9777
- }
9778
- if (path === "/index")
9779
- path = "/";
9780
- if (path === "/") {
9781
- const indexContent = await getRoute("/");
9782
- if (!indexContent) {
9783
- const homeContent = await getRoute("/home");
9784
- if (homeContent) {
9785
- return new Response(null, {
9786
- status: 302,
9787
- headers: {
9788
- Location: "/home",
9789
- ...corsHeaders
9816
+ const body = await req.json();
9817
+ const { Database } = await import("bun:sqlite");
9818
+ const dbPath = nodePath.resolve(process19.cwd(), "database/stacks.sqlite");
9819
+ const db = new Database(dbPath);
9820
+ try {
9821
+ const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
9822
+ const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
9823
+ const columns = [];
9824
+ const placeholders = [];
9825
+ const values = [];
9826
+ for (const col of validColumns) {
9827
+ if (body[col] !== undefined && body[col] !== "") {
9828
+ columns.push(col);
9829
+ placeholders.push("?");
9830
+ values.push(body[col]);
9831
+ }
9790
9832
  }
9791
- });
9833
+ const now = new Date().toISOString();
9834
+ if (tableInfo.some((c) => c.name === "created_at")) {
9835
+ columns.push("created_at");
9836
+ placeholders.push("?");
9837
+ values.push(now);
9838
+ }
9839
+ if (tableInfo.some((c) => c.name === "updated_at")) {
9840
+ columns.push("updated_at");
9841
+ placeholders.push("?");
9842
+ values.push(now);
9843
+ }
9844
+ if (columns.length === 0) {
9845
+ return new Response(JSON.stringify({ error: "No valid fields provided" }), {
9846
+ status: 400,
9847
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9848
+ });
9849
+ }
9850
+ const query = `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
9851
+ const stmt = db.prepare(query);
9852
+ const result = stmt.run(...values);
9853
+ return new Response(JSON.stringify({
9854
+ success: true,
9855
+ id: result.lastInsertRowid,
9856
+ message: "Record created successfully"
9857
+ }), {
9858
+ status: 201,
9859
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9860
+ });
9861
+ } catch (error) {
9862
+ console.error("API Error:", error);
9863
+ return new Response(JSON.stringify({
9864
+ error: error.message || "Failed to create record"
9865
+ }), {
9866
+ status: 500,
9867
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9868
+ });
9869
+ } finally {
9870
+ db.close();
9871
+ }
9872
+ } catch {}
9873
+ }
9874
+ if (path === "/index")
9875
+ path = "/";
9876
+ if (path === "/") {
9877
+ const indexContent = await getRoute("/");
9878
+ if (!indexContent) {
9879
+ const homeContent = await getRoute("/home");
9880
+ if (homeContent) {
9881
+ return new Response(null, {
9882
+ status: 302,
9883
+ headers: {
9884
+ Location: "/home",
9885
+ ...corsHeaders
9886
+ }
9887
+ });
9888
+ }
9792
9889
  }
9793
9890
  }
9794
- }
9795
- const content = await getRoute(path);
9796
- if (content) {
9797
- const isSpaNav = req.headers.get("X-STX-Router") === "true";
9798
- if (isSpaNav) {
9799
- const layoutMatch = content.match(/<!-- stx-layout: ([^ ]+) -->/);
9800
- const pageLayout = layoutMatch ? layoutMatch[1] : "default";
9801
- const pageLayoutGroup = deriveLayoutGroup(pageLayout);
9802
- let fragment = content;
9803
- const headStyles = [];
9804
- const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
9805
- if (headMatch) {
9806
- const headContent = headMatch[1];
9807
- let styleMatch;
9808
- const styleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9809
- while ((styleMatch = styleRe.exec(headContent)) !== null) {
9810
- if (styleMatch[0].includes("data-crosswind")) {
9811
- const cssContent = styleMatch[0].replace(/<style[^>]*>/, "").replace(/<\/style>/, "");
9812
- const hiddenRule = cssContent.indexOf("[hidden]");
9813
- if (hiddenRule !== -1) {
9814
- const afterPreflight = cssContent.indexOf("}", hiddenRule) + 1;
9815
- const utilities = cssContent.slice(afterPreflight).trim();
9816
- if (utilities) {
9817
- headStyles.push(`<style data-crosswind="fragment">${utilities}</style>`);
9891
+ const content = await getRoute(path);
9892
+ if (content) {
9893
+ const isSpaNav = req.headers.get("X-STX-Router") === "true";
9894
+ if (isSpaNav) {
9895
+ const layoutMatch = content.match(/<!-- stx-layout: ([^ ]+) -->/);
9896
+ const pageLayout = layoutMatch ? layoutMatch[1] : "default";
9897
+ const groupMetaMatch = content.match(/<meta\b[^>]*name=["']stx-layout-group["'][^>]*content=["']([^"']+)["'][^>]*>/i);
9898
+ const pageLayoutGroup = i18nConfig && i18nLocale ? `i18n:${i18nLocale}` : groupMetaMatch?.[1] || deriveLayoutGroup(pageLayout);
9899
+ let fragment = content;
9900
+ const headStyles = [];
9901
+ const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
9902
+ if (headMatch) {
9903
+ const headContent = headMatch[1];
9904
+ let styleMatch;
9905
+ const styleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9906
+ while ((styleMatch = styleRe.exec(headContent)) !== null) {
9907
+ if (styleMatch[0].includes("data-crosswind")) {
9908
+ const cssContent = styleMatch[0].replace(/<style[^>]*>/, "").replace(/<\/style>/, "");
9909
+ const hiddenRule = cssContent.indexOf("[hidden]");
9910
+ if (hiddenRule !== -1) {
9911
+ const afterPreflight = cssContent.indexOf("}", hiddenRule) + 1;
9912
+ const utilities = cssContent.slice(afterPreflight).trim();
9913
+ if (utilities) {
9914
+ headStyles.push(`<style data-crosswind="fragment">${utilities}</style>`);
9915
+ }
9818
9916
  }
9917
+ continue;
9819
9918
  }
9820
- continue;
9919
+ headStyles.push(styleMatch[0]);
9821
9920
  }
9822
- headStyles.push(styleMatch[0]);
9823
9921
  }
9824
- }
9825
- const mainOpenMatch = fragment.match(/<main\b[^>]*>/i);
9826
- const mainCloseIdx = fragment.lastIndexOf("</main>");
9827
- if (mainOpenMatch && mainCloseIdx !== -1) {
9828
- const bodyMatch = content.match(/<body\b[^>]*>/i);
9829
- if (bodyMatch) {
9830
- const bodyStart = bodyMatch.index + bodyMatch[0].length;
9831
- const mainIdx = mainOpenMatch.index;
9832
- const beforeMain = content.slice(bodyStart, mainIdx);
9833
- let bodyStyleMatch;
9834
- const bodyStyleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9835
- while ((bodyStyleMatch = bodyStyleRe.exec(beforeMain)) !== null) {
9836
- headStyles.push(bodyStyleMatch[0]);
9922
+ const mainOpenMatch = fragment.match(/<main\b[^>]*>/i);
9923
+ const mainCloseIdx = fragment.lastIndexOf("</main>");
9924
+ if (mainOpenMatch && mainCloseIdx !== -1) {
9925
+ const bodyMatch = content.match(/<body\b[^>]*>/i);
9926
+ if (bodyMatch) {
9927
+ const bodyStart = bodyMatch.index + bodyMatch[0].length;
9928
+ const mainIdx = mainOpenMatch.index;
9929
+ const beforeMain = content.slice(bodyStart, mainIdx);
9930
+ let bodyStyleMatch;
9931
+ const bodyStyleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9932
+ while ((bodyStyleMatch = bodyStyleRe.exec(beforeMain)) !== null) {
9933
+ headStyles.push(bodyStyleMatch[0]);
9934
+ }
9837
9935
  }
9936
+ const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
9937
+ fragment = fragment.slice(mainStart, mainCloseIdx).trim();
9838
9938
  }
9839
- const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
9840
- fragment = fragment.slice(mainStart, mainCloseIdx).trim();
9841
- }
9842
- const pageSetupScripts = [];
9843
- const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
9844
- let setupMatch;
9845
- while ((setupMatch = allScriptRe.exec(content)) !== null) {
9846
- const scriptContent = setupMatch[0];
9847
- if (scriptContent.includes("__stx_early_mounts"))
9848
- continue;
9849
- if (scriptContent.includes("data-stx-reactive") && scriptContent.includes("window.__stx_reactive"))
9850
- continue;
9851
- pageSetupScripts.push(scriptContent);
9852
- }
9853
- const bridgeInitRe = /<script\b[^>]*data-stx-reactive[^>]*>(?![\s\S]*window\.__stx_reactive)[\s\S]*?<\/script>/gi;
9854
- while ((setupMatch = bridgeInitRe.exec(content)) !== null) {
9855
- pageSetupScripts.push(setupMatch[0]);
9856
- }
9857
- fragment = fragment.replace(/<script data-stx-scoped>\s*;?\(function\(\)\s*\{[\s\S]*?<\/script>/g, "");
9858
- const clearStale = "<script data-stx-page>if(window.stx)window.stx._latestSetup=null;</script>";
9859
- fragment = `${headStyles.join(`
9939
+ const pageSetupScripts = [];
9940
+ const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
9941
+ let setupMatch;
9942
+ while ((setupMatch = allScriptRe.exec(content)) !== null) {
9943
+ const scriptContent = setupMatch[0];
9944
+ if (scriptContent.includes("__stx_early_mounts"))
9945
+ continue;
9946
+ if (scriptContent.includes("data-stx-reactive") && scriptContent.includes("window.__stx_reactive"))
9947
+ continue;
9948
+ pageSetupScripts.push(scriptContent);
9949
+ }
9950
+ const bridgeInitRe = /<script\b[^>]*data-stx-reactive[^>]*>(?![\s\S]*window\.__stx_reactive)[\s\S]*?<\/script>/gi;
9951
+ while ((setupMatch = bridgeInitRe.exec(content)) !== null) {
9952
+ pageSetupScripts.push(setupMatch[0]);
9953
+ }
9954
+ fragment = fragment.replace(/<script data-stx-scoped>\s*;?\(function\(\)\s*\{[\s\S]*?<\/script>/g, "");
9955
+ const clearStale = "<script data-stx-page>if(window.stx)window.stx._latestSetup=null;</script>";
9956
+ fragment = `${headStyles.join(`
9860
9957
  `)}
9861
9958
  ${fragment}
9862
9959
  ${clearStale}
9863
9960
  ${pageSetupScripts.join(`
9864
9961
  `)}`;
9865
- return new Response(fragment, {
9962
+ return new Response(fragment, {
9963
+ headers: {
9964
+ "Content-Type": "text/html; charset=utf-8",
9965
+ "X-STX-Fragment": "true",
9966
+ "X-STX-Layout": pageLayout,
9967
+ "X-STX-Layout-Group": pageLayoutGroup,
9968
+ "Cache-Control": "no-store",
9969
+ ...corsHeaders
9970
+ }
9971
+ });
9972
+ }
9973
+ let cleaned = content;
9974
+ let runtimeCount = 0;
9975
+ cleaned = content.replace(/<script data-stx-scoped>\(function\(\)\{'use strict';var cloakStyle[\s\S]*?<\/script>/g, (match) => {
9976
+ runtimeCount++;
9977
+ return runtimeCount === 1 ? match : "";
9978
+ });
9979
+ return new Response(injectHmrClient(cleaned), {
9866
9980
  headers: {
9867
9981
  "Content-Type": "text/html; charset=utf-8",
9868
- "X-STX-Fragment": "true",
9869
- "X-STX-Layout": pageLayout,
9870
- "X-STX-Layout-Group": pageLayoutGroup,
9871
- "Cache-Control": "no-cache",
9982
+ "Cache-Control": "no-store",
9872
9983
  ...corsHeaders
9873
9984
  }
9874
9985
  });
9875
9986
  }
9876
- let cleaned = content;
9877
- let runtimeCount = 0;
9878
- cleaned = content.replace(/<script data-stx-scoped>\(function\(\)\{'use strict';var cloakStyle[\s\S]*?<\/script>/g, (match) => {
9879
- runtimeCount++;
9880
- return runtimeCount === 1 ? match : "";
9881
- });
9882
- return new Response(injectHmrClient(cleaned), {
9883
- headers: {
9884
- "Content-Type": "text/html; charset=utf-8",
9885
- "Cache-Control": "no-cache",
9886
- ...corsHeaders
9887
- }
9888
- });
9889
- }
9890
- const contentWithExt = await getRoute(`${path}.html`);
9891
- if (contentWithExt) {
9892
- return new Response(injectHmrClient(contentWithExt), {
9893
- headers: {
9894
- "Content-Type": "text/html; charset=utf-8",
9895
- "Cache-Control": "no-cache",
9896
- ...corsHeaders
9897
- }
9898
- });
9899
- }
9900
- if (path.startsWith("/chunk-") || path.endsWith(".js") || path.endsWith(".css")) {
9901
- try {
9902
- const buildFile = Bun.file(`.stx${path}`);
9903
- if (await buildFile.exists()) {
9904
- const ext = path.split(".").pop()?.toLowerCase();
9905
- const contentType = ext === "css" ? "text/css" : ext === "js" ? "application/javascript" : "text/plain";
9906
- return new Response(buildFile, {
9907
- headers: {
9908
- "Content-Type": contentType,
9909
- "Cache-Control": "no-cache"
9910
- }
9911
- });
9912
- }
9913
- } catch {}
9914
- }
9915
- if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
9916
- await ensureAssets();
9917
- let assetPathname;
9918
- try {
9919
- assetPathname = decodeURIComponent(path);
9920
- } catch {
9921
- return new Response("Invalid asset path", { status: 400 });
9987
+ const contentWithExt = await getRoute(`${path}.html`);
9988
+ if (contentWithExt) {
9989
+ return new Response(injectHmrClient(contentWithExt), {
9990
+ headers: {
9991
+ "Content-Type": "text/html; charset=utf-8",
9992
+ "Cache-Control": "no-store",
9993
+ ...corsHeaders
9994
+ }
9995
+ });
9922
9996
  }
9923
- if (!isSafeAssetPath(assetPathname))
9924
- return new Response("Invalid asset path", { status: 400 });
9925
- for (const assetPath of assetRequestPaths(assetPathname)) {
9997
+ if (path.startsWith("/chunk-") || path.endsWith(".js") || path.endsWith(".css")) {
9926
9998
  try {
9927
- const filePath = nodePath.resolve(process19.cwd(), `.${assetPath}`);
9928
- const file = Bun.file(filePath);
9929
- if (await file.exists()) {
9930
- const ext = getAssetExtension(assetPath);
9931
- if (isBundledAssetExtension(ext))
9932
- return bundleBrowserAsset(filePath);
9933
- return new Response(file, {
9999
+ const buildFile = Bun.file(`.stx${path}`);
10000
+ if (await buildFile.exists()) {
10001
+ const ext = path.split(".").pop()?.toLowerCase();
10002
+ const contentType = ext === "css" ? "text/css" : ext === "js" ? "application/javascript" : "text/plain";
10003
+ return new Response(buildFile, {
9934
10004
  headers: {
9935
- "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9936
- "Cache-Control": "no-cache"
10005
+ "Content-Type": contentType,
10006
+ "Cache-Control": "no-store"
9937
10007
  }
9938
10008
  });
9939
10009
  }
10010
+ } catch {}
10011
+ }
10012
+ if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
10013
+ await ensureAssets();
10014
+ let assetPathname;
10015
+ try {
10016
+ assetPathname = decodeURIComponent(path);
9940
10017
  } catch {
9941
- continue;
10018
+ return new Response("Invalid asset path", { status: 400 });
9942
10019
  }
9943
- }
9944
- }
9945
- if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
9946
- const publicRoot = nodePath.resolve(process19.cwd(), publicDir);
9947
- let safePathname;
9948
- try {
9949
- safePathname = decodeURIComponent(path);
9950
- } catch {
9951
- safePathname = path;
9952
- }
9953
- if (!safePathname.includes("\x00")) {
9954
- const resolvedPath = nodePath.resolve(publicRoot, `.${safePathname}`);
9955
- const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath.sep}`);
9956
- if (isInsidePublicRoot) {
10020
+ if (!isSafeAssetPath(assetPathname))
10021
+ return new Response("Invalid asset path", { status: 400 });
10022
+ for (const assetPath of assetRequestPaths(assetPathname)) {
9957
10023
  try {
9958
- const file = Bun.file(resolvedPath);
10024
+ const filePath = nodePath.resolve(process19.cwd(), `.${assetPath}`);
10025
+ const file = Bun.file(filePath);
9959
10026
  if (await file.exists()) {
9960
- const stat4 = await file.stat().catch(() => null);
9961
- if (stat4 && !stat4.isDirectory()) {
9962
- const ext = resolvedPath.split(".").pop()?.toLowerCase();
9963
- return new Response(file, {
9964
- headers: {
9965
- "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9966
- "Cache-Control": "no-cache"
9967
- }
9968
- });
9969
- }
10027
+ const ext = getAssetExtension(assetPath);
10028
+ if (isBundledAssetExtension(ext))
10029
+ return bundleBrowserAsset(filePath);
10030
+ return new Response(file, {
10031
+ headers: {
10032
+ "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
10033
+ "Cache-Control": "no-store"
10034
+ }
10035
+ });
9970
10036
  }
9971
- } catch {}
10037
+ } catch {
10038
+ continue;
10039
+ }
9972
10040
  }
9973
10041
  }
9974
- }
9975
- if (path === "/favicon.ico") {
9976
- return new Response(null, { status: 204 });
9977
- }
9978
- const files = await discoverFiles();
9979
- const availableRoutes = [];
9980
- for (const filePath of files) {
9981
- let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
9982
- for (const pattern of patterns) {
9983
- const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
9984
- if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
9985
- normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
9986
- break;
10042
+ if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
10043
+ const publicRoot = nodePath.resolve(process19.cwd(), publicDir);
10044
+ let safePathname;
10045
+ try {
10046
+ safePathname = decodeURIComponent(path);
10047
+ } catch {
10048
+ safePathname = path;
10049
+ }
10050
+ if (!safePathname.includes("\x00")) {
10051
+ const resolvedPath = nodePath.resolve(publicRoot, `.${safePathname}`);
10052
+ const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath.sep}`);
10053
+ if (isInsidePublicRoot) {
10054
+ try {
10055
+ const file = Bun.file(resolvedPath);
10056
+ if (await file.exists()) {
10057
+ const stat4 = await file.stat().catch(() => null);
10058
+ if (stat4 && !stat4.isDirectory()) {
10059
+ const ext = resolvedPath.split(".").pop()?.toLowerCase();
10060
+ return new Response(file, {
10061
+ headers: {
10062
+ "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
10063
+ "Cache-Control": "no-store"
10064
+ }
10065
+ });
10066
+ }
10067
+ }
10068
+ } catch {}
10069
+ }
9987
10070
  }
9988
10071
  }
9989
- let route2 = normalizedPath.replace(/\.(stx|md|html)$/, "");
9990
- if (route2.startsWith("pages/")) {
9991
- route2 = route2.slice(6);
10072
+ if (path === "/favicon.ico") {
10073
+ return new Response(null, { status: 204 });
9992
10074
  }
9993
- availableRoutes.push(`<li><a href="/${route2}">/${route2}</a></li>`);
9994
- }
9995
- const routesList = availableRoutes.join(`
10075
+ const files = await discoverFiles();
10076
+ const availableRoutes = [];
10077
+ for (const filePath of files) {
10078
+ let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
10079
+ for (const pattern of patterns) {
10080
+ const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
10081
+ if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
10082
+ normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
10083
+ break;
10084
+ }
10085
+ }
10086
+ let route2 = normalizedPath.replace(/\.(stx|md|html)$/, "");
10087
+ if (route2.startsWith("pages/")) {
10088
+ route2 = route2.slice(6);
10089
+ }
10090
+ availableRoutes.push(`<li><a href="/${route2}">/${route2}</a></li>`);
10091
+ }
10092
+ const routesList = availableRoutes.join(`
9996
10093
  `);
9997
- return new Response(injectHmrClient(`
10094
+ return new Response(injectHmrClient(`
9998
10095
  <!DOCTYPE html>
9999
10096
  <html>
10000
10097
  <head>
@@ -10022,9 +10119,13 @@ ${pageSetupScripts.join(`
10022
10119
  </body>
10023
10120
  </html>
10024
10121
  `), {
10025
- status: 404,
10026
- headers: { "Content-Type": "text/html; charset=utf-8" }
10027
- });
10122
+ status: 404,
10123
+ headers: { "Content-Type": "text/html; charset=utf-8" }
10124
+ });
10125
+ } finally {
10126
+ activeServeLocale = null;
10127
+ activeServeSearch = "";
10128
+ }
10028
10129
  })();
10029
10130
  return applyI18nToResponse(_i18nResp, i18nLocale ?? (i18nConfig?.defaultLocale ?? "en"), path);
10030
10131
  }
@@ -10098,5 +10199,6 @@ if (import.meta.main) {
10098
10199
  }
10099
10200
  export {
10100
10201
  serve,
10101
- bundleBrowserAsset
10202
+ bundleBrowserAsset,
10203
+ buildDynamicRouteRegexes
10102
10204
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.67",
4
+ "version": "0.2.70",
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",
@@ -73,7 +73,7 @@
73
73
  "typecheck": "bun tsc --noEmit"
74
74
  },
75
75
  "dependencies": {
76
- "@stacksjs/stx": "0.2.67",
77
- "stx-router": "0.2.67"
76
+ "@stacksjs/stx": "0.2.70",
77
+ "stx-router": "0.2.70"
78
78
  }
79
79
  }