bun-plugin-stx 0.2.66 → 0.2.69

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,22 +8782,25 @@ 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
  }
8780
8796
  async function serve(options) {
8781
8797
  const stxConfig = await loadConfig6({
8782
8798
  name: "stx",
8799
+ alias: ["ui"],
8783
8800
  cwd: process19.cwd(),
8784
- defaultConfig: defaultStxConfig
8801
+ defaultConfig: defaultStxConfig,
8802
+ checkEnv: false,
8803
+ verbose: false
8785
8804
  });
8786
8805
  try {
8787
8806
  const stxMod = options.stxModule ? options.stxModule : await defaultStxModule;
@@ -8816,8 +8835,10 @@ After installing: bun add bun-plugin-stx`);
8816
8835
  }
8817
8836
  const routes = new Map;
8818
8837
  let sourceFiles = null;
8838
+ let discoveredPagePaths = null;
8819
8839
  let assetsInitialized = false;
8820
8840
  const htmlCache = new Map;
8841
+ const ENABLE_HTML_CACHE = false;
8821
8842
  const crosswindCssCache = new Map;
8822
8843
  const hmrClients = new Set;
8823
8844
  const hmrEncoder = new TextEncoder;
@@ -8988,6 +9009,31 @@ After installing: bun add bun-plugin-stx`);
8988
9009
  const authRedirectTo = authConfig?.redirectTo ?? "/login";
8989
9010
  const authHome = authConfig?.home ?? "/";
8990
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
+ }
8991
9037
  const builtInMiddleware = authConfig === null ? {} : {
8992
9038
  auth: (_req, ctx) => {
8993
9039
  const tok = ctx.cookies[authCookieName];
@@ -9024,6 +9070,37 @@ After installing: bun add bun-plugin-stx`);
9024
9070
  return out;
9025
9071
  }
9026
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
+ }
9027
9104
  async function discoverFiles() {
9028
9105
  if (sourceFiles !== null)
9029
9106
  return sourceFiles;
@@ -9059,14 +9136,16 @@ After installing: bun add bun-plugin-stx`);
9059
9136
  }
9060
9137
  }
9061
9138
  sourceFiles = files;
9139
+ rebuildDiscoveredPagePaths(files);
9062
9140
  pageMiddlewareByPath.clear();
9063
9141
  pageMiddlewarePatterns.length = 0;
9064
9142
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9065
9143
  try {
9066
- const { Router } = await import("./chunk-wv4bcqba.js");
9144
+ const { Router } = await import("./chunk-sm4gbwrd.js");
9067
9145
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9068
9146
  const router = new Router(process19.cwd(), { pagesDirs });
9069
- console.log(`[stx] Generated ${router.routes.length} routes \u2192 .stx/routes.ts`);
9147
+ if (!options.quiet)
9148
+ console.log(`[stx] Generated ${router.routes.length} routes \u2192 .stx/routes.ts`);
9070
9149
  } catch (e) {}
9071
9150
  return files;
9072
9151
  }
@@ -9161,7 +9240,9 @@ After installing: bun add bun-plugin-stx`);
9161
9240
  crosswindUserConfigPromise = loadConfig6({
9162
9241
  name: "crosswind",
9163
9242
  cwd: process19.cwd(),
9164
- defaultConfig: {}
9243
+ defaultConfig: {},
9244
+ checkEnv: false,
9245
+ verbose: false
9165
9246
  });
9166
9247
  }
9167
9248
  const userConfig = await crosswindUserConfigPromise;
@@ -9229,10 +9310,14 @@ After installing: bun add bun-plugin-stx`);
9229
9310
  }
9230
9311
  }
9231
9312
  async function processTemplate(filePath) {
9232
- const cachedEntry = htmlCache.get(filePath);
9233
- if (cachedEntry && await templateSignatureFresh(cachedEntry.signature))
9234
- return cachedEntry.html;
9235
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
+ }
9236
9321
  const serverScriptRegex = /<script\b([^>]*)\bserver\b[^>]*>([\s\S]*?)<\/script>/gi;
9237
9322
  const serverScripts = [];
9238
9323
  let scriptMatch;
@@ -9246,6 +9331,8 @@ After installing: bun add bun-plugin-stx`);
9246
9331
  };
9247
9332
  const { processDirectives, extractVariables, defaultConfig: defaultConfig4, generateSpaShell, injectRouterScript, resetHead } = await stxModule;
9248
9333
  resetHead();
9334
+ injectServeLocaleContext(context);
9335
+ injectServeRequestContext(context);
9249
9336
  for (const scriptBody of serverScripts) {
9250
9337
  await extractVariables(scriptBody, context, filePath);
9251
9338
  }
@@ -9278,7 +9365,7 @@ After installing: bun add bun-plugin-stx`);
9278
9365
  let output = templateContent;
9279
9366
  const dependencies = new Set;
9280
9367
  output = await processDirectives(output, context, filePath, config5, dependencies);
9281
- output = await injectRouterScript(output);
9368
+ output = await injectRouterScript(output, getRouterInjectOptions());
9282
9369
  const directiveTemplateRe = /@for|:for|@if|:if|x-for|x-if/;
9283
9370
  const hasDirectiveTemplates = /<template\s[^>]*(?:@for|:for|@if|:if|x-for|x-if)/.test(output);
9284
9371
  if (hasDirectiveTemplates) {
@@ -9292,9 +9379,9 @@ After installing: bun add bun-plugin-stx`);
9292
9379
  } else {
9293
9380
  output = output.replace(/<template[^>]*>/gi, "").replace(/<\/template>/gi, "");
9294
9381
  }
9295
- if (!context.__stx_skip_cache) {
9382
+ if (ENABLE_HTML_CACHE && !context.__stx_skip_cache && !skipCacheHint) {
9296
9383
  const signature = await buildTemplateSignature(filePath, dependencies);
9297
- htmlCache.set(filePath, { html: output, signature });
9384
+ htmlCache.set(htmlCacheKey(filePath), { html: output, signature });
9298
9385
  }
9299
9386
  return output;
9300
9387
  }
@@ -9378,25 +9465,7 @@ After installing: bun add bun-plugin-stx`);
9378
9465
  }
9379
9466
  }
9380
9467
  const fileRouteBase = relativeFilePath.replace(/\.(stx|md|html)$/, "");
9381
- const routePattern = fileRouteBase.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9382
- const regexPatterns = [
9383
- new RegExp(`^${routePattern}$`)
9384
- ];
9385
- const fileRouteNoIndex = fileRouteBase.replace(/\/index$/, "");
9386
- if (fileRouteNoIndex !== fileRouteBase) {
9387
- const noIndexPattern = fileRouteNoIndex.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9388
- regexPatterns.push(new RegExp(`^${noIndexPattern}$`));
9389
- }
9390
- if (fileRouteBase.startsWith("pages/")) {
9391
- const prettyBase = fileRouteBase.slice(6);
9392
- const prettyPattern = prettyBase.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9393
- regexPatterns.push(new RegExp(`^${prettyPattern}$`));
9394
- const prettyNoIndex = prettyBase.replace(/\/index$/, "");
9395
- if (prettyNoIndex !== prettyBase) {
9396
- const prettyNoIndexPattern = prettyNoIndex.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9397
- regexPatterns.push(new RegExp(`^${prettyNoIndexPattern}$`));
9398
- }
9399
- }
9468
+ const regexPatterns = buildDynamicRouteRegexes(fileRouteBase);
9400
9469
  for (const regex of regexPatterns) {
9401
9470
  const match = normalizedPath.match(regex);
9402
9471
  if (match) {
@@ -9429,6 +9498,7 @@ After installing: bun add bun-plugin-stx`);
9429
9498
  }
9430
9499
  const { processDirectives, extractVariables, defaultConfig: defaultConfig4, injectRouterScript: injectRouter, resetHead: resetHeadDyn } = await stxModule;
9431
9500
  resetHeadDyn();
9501
+ injectServeLocaleContext(context);
9432
9502
  for (const scriptBody of dynServerScripts) {
9433
9503
  await extractVariables(scriptBody, context, filePath);
9434
9504
  }
@@ -9468,7 +9538,7 @@ After installing: bun add bun-plugin-stx`);
9468
9538
  ${output}`;
9469
9539
  }
9470
9540
  }
9471
- output = await injectRouter(output);
9541
+ output = await injectRouter(output, getRouterInjectOptions());
9472
9542
  const directiveTemplateRe2 = /@for|:for|@if|:if|x-for|x-if/;
9473
9543
  output = output.replace(/<template(?:\s[^>]*)?>|<\/template>/gi, (match) => {
9474
9544
  if (directiveTemplateRe2.test(match))
@@ -9506,6 +9576,28 @@ ${output}`;
9506
9576
  }
9507
9577
  return { locale: i18nConfig.defaultLocale, path: pathname };
9508
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
+ }
9509
9601
  async function applyI18nToResponse(res, locale, normalizedPath) {
9510
9602
  if (!siteStxPromise)
9511
9603
  return res;
@@ -9536,8 +9628,8 @@ ${output}`;
9536
9628
  html = `${groupMeta}
9537
9629
  ${html}`;
9538
9630
  }
9539
- if (i18nConfig && locale !== i18nConfig.defaultLocale && siteConfig?.pages) {
9540
- const knownPaths = new Set(Object.keys(siteConfig.pages));
9631
+ const knownPaths = discoveredPagePaths ?? new Set;
9632
+ if (i18nConfig && locale !== i18nConfig.defaultLocale && knownPaths.size > 0) {
9541
9633
  html = html.replace(/<a\b([^>]*?)\bhref="(\/[^"]*)"([^>]*)>/gi, (full, before, href, after) => {
9542
9634
  if (href.startsWith("//"))
9543
9635
  return full;
@@ -9556,6 +9648,7 @@ ${html}`;
9556
9648
  }
9557
9649
  if (i18nConfig && stx.applyTranslations)
9558
9650
  html = stx.applyTranslations(html, i18nConfig, locale);
9651
+ html = ensureRouterConfig(html);
9559
9652
  const headers = new Headers(res.headers);
9560
9653
  headers.delete("content-length");
9561
9654
  return new Response(html, {
@@ -9584,411 +9677,421 @@ ${html}`;
9584
9677
  if (stripped.path !== url.pathname) {
9585
9678
  const rewritten = new URL(req.url);
9586
9679
  rewritten.pathname = stripped.path;
9587
- 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" });
9588
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 });
9589
9688
  }
9590
9689
  }
9591
9690
  const _i18nResp = await (async () => {
9592
- if (req.method === "OPTIONS") {
9593
- return new Response(null, { headers: corsHeaders });
9594
- }
9595
- if (path === "/_stx/hmr") {
9596
- let controller;
9597
- let keepalive;
9598
- const stream = new ReadableStream({
9599
- start(c) {
9600
- controller = c;
9601
- hmrClients.add(c);
9602
- 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"}
9603
9705
 
9604
9706
  `));
9605
- keepalive = setInterval(() => {
9606
- try {
9607
- c.enqueue(hmrEncoder.encode(`: keepalive
9707
+ keepalive = setInterval(() => {
9708
+ try {
9709
+ c.enqueue(hmrEncoder.encode(`: keepalive
9608
9710
 
9609
9711
  `));
9610
- } catch {}
9611
- }, 15000);
9612
- },
9613
- cancel() {
9614
- if (keepalive)
9615
- clearInterval(keepalive);
9616
- if (controller)
9617
- hmrClients.delete(controller);
9618
- }
9619
- });
9620
- return new Response(stream, {
9621
- headers: {
9622
- "Content-Type": "text/event-stream",
9623
- "Cache-Control": "no-cache, no-transform",
9624
- Connection: "keep-alive",
9625
- "X-Accel-Buffering": "no"
9626
- }
9627
- });
9628
- }
9629
- if (options.onRequest) {
9630
- const customResponse = await options.onRequest(req);
9631
- if (customResponse)
9632
- return customResponse;
9633
- }
9634
- const route = resolveRouteMiddleware(path);
9635
- const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
9636
- const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
9637
- if (requested.length > 0) {
9638
- const expanded = expandMiddlewareNames(requested);
9639
- const cookies = parseCookies(req);
9640
- const ctx = {
9641
- path,
9642
- url,
9643
- params: route.params,
9644
- cookies,
9645
- redirect: (to, status = 302) => {
9646
- const sep = to.includes("?") ? "&" : "?";
9647
- const next = encodeURIComponent(path + (url.search || ""));
9648
- return new Response(null, {
9649
- status,
9650
- headers: { Location: `${to}${sep}next=${next}` }
9651
- });
9652
- }
9653
- };
9654
- for (const entry of expanded) {
9655
- const colon = entry.indexOf(":");
9656
- const name = colon === -1 ? entry : entry.slice(0, colon);
9657
- const args = colon === -1 ? [] : entry.slice(colon + 1).split(",");
9658
- const handler = middlewareRegistry[name];
9659
- if (!handler) {
9660
- console.warn(`[stx serve] unknown middleware "${name}" on ${path}`);
9661
- continue;
9662
- }
9663
- const result = await handler(req, ctx, ...args);
9664
- if (result instanceof Response)
9665
- 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
+ });
9666
9730
  }
9667
- }
9668
- if (options.routes) {
9669
- const routeHandler = options.routes[path];
9670
- if (routeHandler) {
9671
- return routeHandler(req);
9731
+ if (options.onRequest) {
9732
+ const customResponse = await options.onRequest(req);
9733
+ if (customResponse)
9734
+ return customResponse;
9672
9735
  }
9673
- }
9674
- if (path.startsWith("/_stx/component/")) {
9675
- const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
9676
- if (componentName) {
9677
- try {
9678
- const { processDirectives, defaultConfig: defaultConfig4 } = await stxModule;
9679
- const componentTemplate = `<${componentName} />`;
9680
- const componentOpts = {
9681
- ...defaultConfig4,
9682
- ...componentsDir && { componentsDir },
9683
- ...layoutsDir && { layoutsDir },
9684
- ...partialsDir && { partialsDir },
9685
- autoShell: false
9686
- };
9687
- const html = await processDirectives(componentTemplate, {}, `${componentsDir}/${componentName}.stx`, componentOpts, new Set);
9688
- return new Response(html, {
9689
- headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9690
- });
9691
- } catch (e) {
9692
- return new Response(`<div class="stx-async-error">${e.message}</div>`, {
9693
- status: 500,
9694
- headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9695
- });
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;
9696
9768
  }
9697
9769
  }
9698
- }
9699
- if (path.startsWith("/api/data/") && req.method === "POST") {
9700
- try {
9701
- const tableName = path.replace("/api/data/", "").split("/")[0];
9702
- if (!tableName) {
9703
- return new Response(JSON.stringify({ error: "Table name required" }), {
9704
- status: 400,
9705
- headers: { "Content-Type": "application/json", ...corsHeaders }
9706
- });
9770
+ if (options.routes) {
9771
+ const routeHandler = options.routes[path];
9772
+ if (routeHandler) {
9773
+ return routeHandler(req);
9707
9774
  }
9708
- if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
9709
- return new Response(JSON.stringify({ error: "Invalid table name" }), {
9710
- status: 400,
9711
- headers: { "Content-Type": "application/json", ...corsHeaders }
9712
- });
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
+ }
9713
9799
  }
9714
- const body = await req.json();
9715
- const { Database } = await import("bun:sqlite");
9716
- const dbPath = nodePath.resolve(process19.cwd(), "database/stacks.sqlite");
9717
- const db = new Database(dbPath);
9800
+ }
9801
+ if (path.startsWith("/api/data/") && req.method === "POST") {
9718
9802
  try {
9719
- const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
9720
- const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
9721
- const columns = [];
9722
- const placeholders = [];
9723
- const values = [];
9724
- for (const col of validColumns) {
9725
- if (body[col] !== undefined && body[col] !== "") {
9726
- columns.push(col);
9727
- placeholders.push("?");
9728
- values.push(body[col]);
9729
- }
9730
- }
9731
- const now = new Date().toISOString();
9732
- if (tableInfo.some((c) => c.name === "created_at")) {
9733
- columns.push("created_at");
9734
- placeholders.push("?");
9735
- values.push(now);
9736
- }
9737
- if (tableInfo.some((c) => c.name === "updated_at")) {
9738
- columns.push("updated_at");
9739
- placeholders.push("?");
9740
- 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
+ });
9741
9809
  }
9742
- if (columns.length === 0) {
9743
- 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" }), {
9744
9812
  status: 400,
9745
9813
  headers: { "Content-Type": "application/json", ...corsHeaders }
9746
9814
  });
9747
9815
  }
9748
- const query = `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
9749
- const stmt = db.prepare(query);
9750
- const result = stmt.run(...values);
9751
- return new Response(JSON.stringify({
9752
- success: true,
9753
- id: result.lastInsertRowid,
9754
- message: "Record created successfully"
9755
- }), {
9756
- status: 201,
9757
- headers: { "Content-Type": "application/json", ...corsHeaders }
9758
- });
9759
- } catch (error) {
9760
- console.error("API Error:", error);
9761
- return new Response(JSON.stringify({
9762
- error: error.message || "Failed to create record"
9763
- }), {
9764
- status: 500,
9765
- headers: { "Content-Type": "application/json", ...corsHeaders }
9766
- });
9767
- } finally {
9768
- db.close();
9769
- }
9770
- } catch {}
9771
- }
9772
- if (path === "/index")
9773
- path = "/";
9774
- if (path === "/") {
9775
- const indexContent = await getRoute("/");
9776
- if (!indexContent) {
9777
- const homeContent = await getRoute("/home");
9778
- if (homeContent) {
9779
- return new Response(null, {
9780
- status: 302,
9781
- headers: {
9782
- Location: "/home",
9783
- ...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
+ }
9784
9832
  }
9785
- });
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
+ }
9786
9889
  }
9787
9890
  }
9788
- }
9789
- const content = await getRoute(path);
9790
- if (content) {
9791
- const isSpaNav = req.headers.get("X-STX-Router") === "true";
9792
- if (isSpaNav) {
9793
- const layoutMatch = content.match(/<!-- stx-layout: ([^ ]+) -->/);
9794
- const pageLayout = layoutMatch ? layoutMatch[1] : "default";
9795
- const pageLayoutGroup = deriveLayoutGroup(pageLayout);
9796
- let fragment = content;
9797
- const headStyles = [];
9798
- const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
9799
- if (headMatch) {
9800
- const headContent = headMatch[1];
9801
- let styleMatch;
9802
- const styleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9803
- while ((styleMatch = styleRe.exec(headContent)) !== null) {
9804
- if (styleMatch[0].includes("data-crosswind")) {
9805
- const cssContent = styleMatch[0].replace(/<style[^>]*>/, "").replace(/<\/style>/, "");
9806
- const hiddenRule = cssContent.indexOf("[hidden]");
9807
- if (hiddenRule !== -1) {
9808
- const afterPreflight = cssContent.indexOf("}", hiddenRule) + 1;
9809
- const utilities = cssContent.slice(afterPreflight).trim();
9810
- if (utilities) {
9811
- 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
+ }
9812
9916
  }
9917
+ continue;
9813
9918
  }
9814
- continue;
9919
+ headStyles.push(styleMatch[0]);
9815
9920
  }
9816
- headStyles.push(styleMatch[0]);
9817
9921
  }
9818
- }
9819
- const mainOpenMatch = fragment.match(/<main\b[^>]*>/i);
9820
- const mainCloseIdx = fragment.lastIndexOf("</main>");
9821
- if (mainOpenMatch && mainCloseIdx !== -1) {
9822
- const bodyMatch = content.match(/<body\b[^>]*>/i);
9823
- if (bodyMatch) {
9824
- const bodyStart = bodyMatch.index + bodyMatch[0].length;
9825
- const mainIdx = mainOpenMatch.index;
9826
- const beforeMain = content.slice(bodyStart, mainIdx);
9827
- let bodyStyleMatch;
9828
- const bodyStyleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9829
- while ((bodyStyleMatch = bodyStyleRe.exec(beforeMain)) !== null) {
9830
- 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
+ }
9831
9935
  }
9936
+ const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
9937
+ fragment = fragment.slice(mainStart, mainCloseIdx).trim();
9832
9938
  }
9833
- const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
9834
- fragment = fragment.slice(mainStart, mainCloseIdx).trim();
9835
- }
9836
- const pageSetupScripts = [];
9837
- const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
9838
- let setupMatch;
9839
- while ((setupMatch = allScriptRe.exec(content)) !== null) {
9840
- const scriptContent = setupMatch[0];
9841
- if (scriptContent.includes("__stx_early_mounts"))
9842
- continue;
9843
- if (scriptContent.includes("data-stx-reactive") && scriptContent.includes("window.__stx_reactive"))
9844
- continue;
9845
- pageSetupScripts.push(scriptContent);
9846
- }
9847
- const bridgeInitRe = /<script\b[^>]*data-stx-reactive[^>]*>(?![\s\S]*window\.__stx_reactive)[\s\S]*?<\/script>/gi;
9848
- while ((setupMatch = bridgeInitRe.exec(content)) !== null) {
9849
- pageSetupScripts.push(setupMatch[0]);
9850
- }
9851
- fragment = fragment.replace(/<script data-stx-scoped>\s*;?\(function\(\)\s*\{[\s\S]*?<\/script>/g, "");
9852
- const clearStale = "<script data-stx-page>if(window.stx)window.stx._latestSetup=null;</script>";
9853
- 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(`
9854
9957
  `)}
9855
9958
  ${fragment}
9856
9959
  ${clearStale}
9857
9960
  ${pageSetupScripts.join(`
9858
9961
  `)}`;
9859
- 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), {
9860
9980
  headers: {
9861
9981
  "Content-Type": "text/html; charset=utf-8",
9862
- "X-STX-Fragment": "true",
9863
- "X-STX-Layout": pageLayout,
9864
- "X-STX-Layout-Group": pageLayoutGroup,
9865
- "Cache-Control": "no-cache",
9982
+ "Cache-Control": "no-store",
9866
9983
  ...corsHeaders
9867
9984
  }
9868
9985
  });
9869
9986
  }
9870
- let cleaned = content;
9871
- let runtimeCount = 0;
9872
- cleaned = content.replace(/<script data-stx-scoped>\(function\(\)\{'use strict';var cloakStyle[\s\S]*?<\/script>/g, (match) => {
9873
- runtimeCount++;
9874
- return runtimeCount === 1 ? match : "";
9875
- });
9876
- return new Response(injectHmrClient(cleaned), {
9877
- headers: {
9878
- "Content-Type": "text/html; charset=utf-8",
9879
- "Cache-Control": "no-cache",
9880
- ...corsHeaders
9881
- }
9882
- });
9883
- }
9884
- const contentWithExt = await getRoute(`${path}.html`);
9885
- if (contentWithExt) {
9886
- return new Response(injectHmrClient(contentWithExt), {
9887
- headers: {
9888
- "Content-Type": "text/html; charset=utf-8",
9889
- "Cache-Control": "no-cache",
9890
- ...corsHeaders
9891
- }
9892
- });
9893
- }
9894
- if (path.startsWith("/chunk-") || path.endsWith(".js") || path.endsWith(".css")) {
9895
- try {
9896
- const buildFile = Bun.file(`.stx${path}`);
9897
- if (await buildFile.exists()) {
9898
- const ext = path.split(".").pop()?.toLowerCase();
9899
- const contentType = ext === "css" ? "text/css" : ext === "js" ? "application/javascript" : "text/plain";
9900
- return new Response(buildFile, {
9901
- headers: {
9902
- "Content-Type": contentType,
9903
- "Cache-Control": "no-cache"
9904
- }
9905
- });
9906
- }
9907
- } catch {}
9908
- }
9909
- if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
9910
- await ensureAssets();
9911
- let assetPathname;
9912
- try {
9913
- assetPathname = decodeURIComponent(path);
9914
- } catch {
9915
- 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
+ });
9916
9996
  }
9917
- if (!isSafeAssetPath(assetPathname))
9918
- return new Response("Invalid asset path", { status: 400 });
9919
- for (const assetPath of assetRequestPaths(assetPathname)) {
9997
+ if (path.startsWith("/chunk-") || path.endsWith(".js") || path.endsWith(".css")) {
9920
9998
  try {
9921
- const filePath = nodePath.resolve(process19.cwd(), `.${assetPath}`);
9922
- const file = Bun.file(filePath);
9923
- if (await file.exists()) {
9924
- const ext = getAssetExtension(assetPath);
9925
- if (isBundledAssetExtension(ext))
9926
- return bundleBrowserAsset(filePath);
9927
- 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, {
9928
10004
  headers: {
9929
- "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9930
- "Cache-Control": "no-cache"
10005
+ "Content-Type": contentType,
10006
+ "Cache-Control": "no-store"
9931
10007
  }
9932
10008
  });
9933
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);
9934
10017
  } catch {
9935
- continue;
10018
+ return new Response("Invalid asset path", { status: 400 });
9936
10019
  }
9937
- }
9938
- }
9939
- if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
9940
- const publicRoot = nodePath.resolve(process19.cwd(), publicDir);
9941
- let safePathname;
9942
- try {
9943
- safePathname = decodeURIComponent(path);
9944
- } catch {
9945
- safePathname = path;
9946
- }
9947
- if (!safePathname.includes("\x00")) {
9948
- const resolvedPath = nodePath.resolve(publicRoot, `.${safePathname}`);
9949
- const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath.sep}`);
9950
- if (isInsidePublicRoot) {
10020
+ if (!isSafeAssetPath(assetPathname))
10021
+ return new Response("Invalid asset path", { status: 400 });
10022
+ for (const assetPath of assetRequestPaths(assetPathname)) {
9951
10023
  try {
9952
- const file = Bun.file(resolvedPath);
10024
+ const filePath = nodePath.resolve(process19.cwd(), `.${assetPath}`);
10025
+ const file = Bun.file(filePath);
9953
10026
  if (await file.exists()) {
9954
- const stat4 = await file.stat().catch(() => null);
9955
- if (stat4 && !stat4.isDirectory()) {
9956
- const ext = resolvedPath.split(".").pop()?.toLowerCase();
9957
- return new Response(file, {
9958
- headers: {
9959
- "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9960
- "Cache-Control": "no-cache"
9961
- }
9962
- });
9963
- }
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
+ });
9964
10036
  }
9965
- } catch {}
10037
+ } catch {
10038
+ continue;
10039
+ }
9966
10040
  }
9967
10041
  }
9968
- }
9969
- if (path === "/favicon.ico") {
9970
- return new Response(null, { status: 204 });
9971
- }
9972
- const files = await discoverFiles();
9973
- const availableRoutes = [];
9974
- for (const filePath of files) {
9975
- let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
9976
- for (const pattern of patterns) {
9977
- const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
9978
- if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
9979
- normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
9980
- 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
+ }
9981
10070
  }
9982
10071
  }
9983
- let route2 = normalizedPath.replace(/\.(stx|md|html)$/, "");
9984
- if (route2.startsWith("pages/")) {
9985
- route2 = route2.slice(6);
10072
+ if (path === "/favicon.ico") {
10073
+ return new Response(null, { status: 204 });
9986
10074
  }
9987
- availableRoutes.push(`<li><a href="/${route2}">/${route2}</a></li>`);
9988
- }
9989
- 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(`
9990
10093
  `);
9991
- return new Response(injectHmrClient(`
10094
+ return new Response(injectHmrClient(`
9992
10095
  <!DOCTYPE html>
9993
10096
  <html>
9994
10097
  <head>
@@ -10016,9 +10119,13 @@ ${pageSetupScripts.join(`
10016
10119
  </body>
10017
10120
  </html>
10018
10121
  `), {
10019
- status: 404,
10020
- headers: { "Content-Type": "text/html; charset=utf-8" }
10021
- });
10122
+ status: 404,
10123
+ headers: { "Content-Type": "text/html; charset=utf-8" }
10124
+ });
10125
+ } finally {
10126
+ activeServeLocale = null;
10127
+ activeServeSearch = "";
10128
+ }
10022
10129
  })();
10023
10130
  return applyI18nToResponse(_i18nResp, i18nLocale ?? (i18nConfig?.defaultLocale ?? "en"), path);
10024
10131
  }
@@ -10092,5 +10199,6 @@ if (import.meta.main) {
10092
10199
  }
10093
10200
  export {
10094
10201
  serve,
10095
- bundleBrowserAsset
10202
+ bundleBrowserAsset,
10203
+ buildDynamicRouteRegexes
10096
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.66",
4
+ "version": "0.2.69",
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.66",
77
- "stx-router": "0.2.66"
76
+ "@stacksjs/stx": "0.2.69",
77
+ "stx-router": "0.2.69"
78
78
  }
79
79
  }