bun-plugin-stx 0.2.84 → 0.2.86

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.
@@ -889,9 +889,21 @@ else {
889
889
  }
890
890
  }
891
891
 
892
+ // A full document is only safe to partial-swap into the stx shell if it is
893
+ // itself stx-rendered. Pages served by another engine mounted on the same
894
+ // origin (e.g. a docs/blog generator) carry none of these markers; swapping
895
+ // their <main> + styles into the stx shell corrupts the layout. Detect them
896
+ // and hand off to a native full navigation instead.
897
+ function isStxDocument(html){
898
+ return html.indexOf('name="stx-layout"')!==-1
899
+ ||html.indexOf('__stxRouterConfig')!==-1
900
+ ||html.indexOf('data-stx-content')!==-1;
901
+ }
902
+
892
903
  function swap(html,url,pushState,hash){
893
904
  var isFragment=html.indexOf('<!--stx-fragment-->')===0;
894
905
  if(isFragment)html=html.slice('<!--stx-fragment-->'.length);
906
+ if(!isFragment&&!isStxDocument(html)){log('[router] non-stx document \u2014 full navigation to:',url);location.href=url;return}
895
907
  var currentContent=getContainer();
896
908
  log('[router] swap: isFragment='+isFragment+' container='+!!currentContent+' tag='+(currentContent&&currentContent.tagName)+' selector='+containerSel+' htmlLen='+html.length);
897
909
  if(!currentContent){log('[router] no container \u2014 falling back');location.href=url;return}
package/dist/serve.d.ts CHANGED
@@ -1,16 +1,3 @@
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
1
  export declare function buildDynamicRouteRegexes(fileRouteBase: string): RegExp[];
15
2
  /**
16
3
  * Are we serving in production? In production the built-in 404 must not
@@ -55,6 +42,42 @@ export declare function bundleBrowserAsset(entrypoint: string): Promise<Response
55
42
  * @param options Server options with patterns and port
56
43
  */
57
44
  export declare function serve(options: ServeOptions): Promise<void>;
45
+ /**
46
+ * Build the candidate regexes for a dynamic route file (stacksjs/stx#1927).
47
+ *
48
+ * `fileRouteBase` is the route path with the file extension already
49
+ * stripped (e.g. `judges/[id]/profile/index`). Returns one regex per
50
+ * acceptable URL shape:
51
+ * - the literal path with `[param]` → `([^/]+)`
52
+ * - the same with a trailing `/index` removed, so
53
+ * `judges/[id]/profile/index` also matches `judges/35/profile`
54
+ * (without this, nested dynamic routes 404 on hard reload)
55
+ * - both of the above again with a leading `pages/` stripped, for
56
+ * projects that nest routable views under `pages/`
57
+ */
58
+ /**
59
+ * Snapshot of the in-flight request, threaded from the fetch handler down to
60
+ * the `<script server>` render (stacksjs/stacks#1967).
61
+ *
62
+ * The module-level `activeServe*` singletons are NOT safe to read at render
63
+ * time: a render can suspend at an `await` (onRequest hooks, DB queries in
64
+ * server scripts) while a concurrent request — the HMR EventSource opens on
65
+ * every page load — runs its own handler and resets the singletons in its
66
+ * `finally`. Threading an immutable per-request snapshot removes that race;
67
+ * the singletons remain only as a fallback for legacy call sites.
68
+ */
69
+ export declare interface ServeRequestContext {
70
+ url: string
71
+ path: string
72
+ search: string
73
+ host: string
74
+ cookieHeader: string
75
+ cookies: Record<string, string>
76
+ ip: string
77
+ locale: string | null
78
+ params: Record<string, string>
79
+ [key: string]: unknown
80
+ }
58
81
  export declare interface ServeOptions {
59
82
  patterns: string[]
60
83
  port?: number
@@ -71,9 +94,10 @@ export declare interface ServeOptions {
71
94
  routes?: Record<string, (req: Request) => Response | Promise<Response>>
72
95
  onRequest?: (req: Request) =>
73
96
  | Response
97
+ | Record<string, unknown>
74
98
  | null
75
99
  | undefined
76
- | Promise<Response | null | undefined>
100
+ | Promise<Response | Record<string, unknown> | null | undefined>
77
101
  middleware?: Record<string, MiddlewareHandler>
78
102
  globalMiddleware?: string[]
79
103
  middlewareGroups?: Record<string, string[]>
package/dist/serve.js CHANGED
@@ -9133,16 +9133,31 @@ After installing: bun add bun-plugin-stx`);
9133
9133
  return typeof translate === "function" ? translate(key, values, loc) : key;
9134
9134
  };
9135
9135
  }
9136
- function injectServeRequestContext(context) {
9137
- if (activeServeSearch) {
9138
- context.__stxServeSearch = activeServeSearch;
9139
- globalThis.__stxServeSearch = activeServeSearch;
9140
- }
9141
- if (activeServeHost)
9142
- context.host = activeServeHost;
9143
- context.cookies = activeServeCookies;
9144
- if (activeServeIp)
9145
- context.ip = activeServeIp;
9136
+ function injectServeRequestContext(context, reqCtx) {
9137
+ const search = reqCtx ? reqCtx.search : activeServeSearch;
9138
+ const host = reqCtx ? reqCtx.host : activeServeHost;
9139
+ const cookies = reqCtx ? reqCtx.cookies : activeServeCookies;
9140
+ const ip = reqCtx ? reqCtx.ip : activeServeIp;
9141
+ context.__stxServeSearch = search;
9142
+ globalThis.__stxServeSearch = search;
9143
+ if (host)
9144
+ context.host = host;
9145
+ context.cookies = cookies;
9146
+ if (ip)
9147
+ context.ip = ip;
9148
+ const full = reqCtx ?? {
9149
+ url: "",
9150
+ path: "",
9151
+ search,
9152
+ host,
9153
+ cookieHeader: activeServeCookieHeader,
9154
+ cookies,
9155
+ ip,
9156
+ locale: activeServeLocale,
9157
+ params: {}
9158
+ };
9159
+ context.__stxServeContext = full;
9160
+ globalThis.__stxServeContext = full;
9146
9161
  }
9147
9162
  function htmlCacheKey(filePath) {
9148
9163
  const loc = i18nConfig ? activeServeLocale ?? i18nConfig.defaultLocale : "";
@@ -9260,7 +9275,7 @@ After installing: bun add bun-plugin-stx`);
9260
9275
  pageMiddlewarePatterns.length = 0;
9261
9276
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9262
9277
  try {
9263
- const { Router } = await import("./chunk-sm4gbwrd.js");
9278
+ const { Router } = await import("./chunk-394anrmt.js");
9264
9279
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9265
9280
  const router = new Router(process19.cwd(), { pagesDirs });
9266
9281
  if (!options.quiet)
@@ -9428,7 +9443,7 @@ After installing: bun add bun-plugin-stx`);
9428
9443
  return "";
9429
9444
  }
9430
9445
  }
9431
- async function processTemplate(filePath) {
9446
+ async function processTemplate(filePath, reqCtx) {
9432
9447
  const content = await Bun.file(filePath).text();
9433
9448
  const skipCacheHint = /(?:^|[^\w$])__stx_skip_cache\s*=\s*true/.test(content);
9434
9449
  if (ENABLE_HTML_CACHE && !skipCacheHint) {
@@ -9451,7 +9466,8 @@ After installing: bun add bun-plugin-stx`);
9451
9466
  const { processDirectives, extractVariables, defaultConfig: defaultConfig4, generateSpaShell, injectRouterScript, resetHead } = await stxModule;
9452
9467
  resetHead();
9453
9468
  injectServeLocaleContext(context);
9454
- injectServeRequestContext(context);
9469
+ injectServeRequestContext(context, reqCtx);
9470
+ context.params = reqCtx?.params ?? {};
9455
9471
  for (const scriptBody of serverScripts) {
9456
9472
  await extractVariables(scriptBody, context, filePath);
9457
9473
  }
@@ -9507,7 +9523,7 @@ After installing: bun add bun-plugin-stx`);
9507
9523
  }
9508
9524
  return output;
9509
9525
  }
9510
- async function getRoute(requestPath) {
9526
+ async function getRoute(requestPath, reqCtx) {
9511
9527
  const files = await discoverFiles();
9512
9528
  let normalizedPath = requestPath.startsWith("/") ? requestPath.slice(1) : requestPath;
9513
9529
  const possibleFiles = [];
@@ -9542,7 +9558,7 @@ After installing: bun add bun-plugin-stx`);
9542
9558
  }
9543
9559
  }
9544
9560
  if (normalizedFilePath === possible || relativeFilePath === possible) {
9545
- const output = await processTemplate(filePath);
9561
+ const output = await processTemplate(filePath, reqCtx);
9546
9562
  return output;
9547
9563
  }
9548
9564
  }
@@ -9561,14 +9577,14 @@ After installing: bun add bun-plugin-stx`);
9561
9577
  const isIndexFile = fileRoute === "index" || fileRoute.endsWith("/index");
9562
9578
  const isRootRequest = requestPath === "/" || normalizedPath === "";
9563
9579
  if (`/${fileRoute}` === requestPath || fileRoute === normalizedPath || `/${fileRoute}.stx` === requestPath || `/${fileRoute}.md` === requestPath || `/${fileRoute}.html` === requestPath || isIndexFile && isRootRequest) {
9564
- const output = await processTemplate(filePath);
9580
+ const output = await processTemplate(filePath, reqCtx);
9565
9581
  routes.set(requestPath, output);
9566
9582
  return output;
9567
9583
  }
9568
9584
  if (fileRoute.startsWith("pages/")) {
9569
9585
  const prettyRoute = fileRoute.slice(6);
9570
9586
  if (`/${prettyRoute}` === requestPath || prettyRoute === normalizedPath) {
9571
- const output = await processTemplate(filePath);
9587
+ const output = await processTemplate(filePath, reqCtx);
9572
9588
  routes.set(requestPath, output);
9573
9589
  return output;
9574
9590
  }
@@ -9593,7 +9609,7 @@ After installing: bun add bun-plugin-stx`);
9593
9609
  if (match) {
9594
9610
  const paramNames = [...fileRouteBase.matchAll(/\[([^\]]+)\]/g)].map((m) => m[1]);
9595
9611
  const paramValues = match.slice(1);
9596
- const output = await processTemplateDynamic(filePath, paramNames, paramValues, normalizedPath);
9612
+ const output = await processTemplateDynamic(filePath, paramNames, paramValues, normalizedPath, reqCtx);
9597
9613
  routes.set(requestPath, output);
9598
9614
  return output;
9599
9615
  }
@@ -9601,7 +9617,7 @@ After installing: bun add bun-plugin-stx`);
9601
9617
  }
9602
9618
  return null;
9603
9619
  }
9604
- async function processTemplateDynamic(filePath, paramNames, paramValues, routePath) {
9620
+ async function processTemplateDynamic(filePath, paramNames, paramValues, routePath, reqCtx) {
9605
9621
  const content = await Bun.file(filePath).text();
9606
9622
  const serverScriptRegex = /<script\b([^>]*)\bserver\b[^>]*>([\s\S]*?)<\/script>/gi;
9607
9623
  const dynServerScripts = [];
@@ -9615,13 +9631,24 @@ After installing: bun add bun-plugin-stx`);
9615
9631
  __dirname: nodePath.dirname(filePath),
9616
9632
  __route: routePath
9617
9633
  };
9634
+ const paramsObj = {};
9618
9635
  for (let i = 0;i < paramNames.length; i++) {
9619
- context[paramNames[i]] = paramValues[i];
9636
+ let value = paramValues[i] ?? "";
9637
+ try {
9638
+ value = decodeURIComponent(value);
9639
+ } catch {}
9640
+ paramsObj[paramNames[i]] = value;
9620
9641
  }
9642
+ for (const name of paramNames) {
9643
+ context[name] = paramsObj[name];
9644
+ }
9645
+ context.params = paramsObj;
9646
+ if (reqCtx)
9647
+ reqCtx.params = paramsObj;
9621
9648
  const { processDirectives, extractVariables, defaultConfig: defaultConfig4, injectRouterScript: injectRouter, resetHead: resetHeadDyn } = await stxModule;
9622
9649
  resetHeadDyn();
9623
9650
  injectServeLocaleContext(context);
9624
- injectServeRequestContext(context);
9651
+ injectServeRequestContext(context, reqCtx);
9625
9652
  for (const scriptBody of dynServerScripts) {
9626
9653
  await extractVariables(scriptBody, context, filePath);
9627
9654
  }
@@ -9651,10 +9678,6 @@ After installing: bun add bun-plugin-stx`);
9651
9678
  const dependencies = new Set;
9652
9679
  output = await processDirectives(output, context, filePath, config5, dependencies);
9653
9680
  if (paramNames.length > 0) {
9654
- const paramsObj = {};
9655
- for (let i = 0;i < paramNames.length; i++) {
9656
- paramsObj[paramNames[i]] = paramValues[i];
9657
- }
9658
9681
  const paramsScript = `<script>if(window.stx)window.stx._rp=${JSON.stringify(paramsObj)};else window.__stx_rp=${JSON.stringify(paramsObj)};</script>`;
9659
9682
  if (output.includes("</head>")) {
9660
9683
  output = output.replace("</head>", `${paramsScript}
@@ -9846,6 +9869,17 @@ ${html}`;
9846
9869
  activeServeCookieHeader = req.headers.get("cookie") || "";
9847
9870
  activeServeCookies = parseCookies(req);
9848
9871
  activeServeIp = server.requestIP(req)?.address || (req.headers.get("x-forwarded-for") || "").split(",")[0].trim();
9872
+ const reqCtx = {
9873
+ url: req.url,
9874
+ path,
9875
+ search: activeServeSearch,
9876
+ host: activeServeHost,
9877
+ cookieHeader: activeServeCookieHeader,
9878
+ cookies: activeServeCookies,
9879
+ ip: activeServeIp,
9880
+ locale: activeServeLocale,
9881
+ params: {}
9882
+ };
9849
9883
  try {
9850
9884
  if (req.method === "OPTIONS") {
9851
9885
  return new Response(null, { headers: corsHeaders });
@@ -9866,7 +9900,7 @@ ${html}`;
9866
9900
 
9867
9901
  `));
9868
9902
  } catch {}
9869
- }, 15000);
9903
+ }, 5000);
9870
9904
  },
9871
9905
  cancel() {
9872
9906
  if (keepalive)
@@ -9885,9 +9919,11 @@ ${html}`;
9885
9919
  });
9886
9920
  }
9887
9921
  if (options.onRequest) {
9888
- const customResponse = await options.onRequest(req);
9889
- if (customResponse)
9890
- return customResponse;
9922
+ const hookResult = await options.onRequest(req);
9923
+ if (hookResult instanceof Response)
9924
+ return hookResult;
9925
+ if (hookResult && typeof hookResult === "object")
9926
+ Object.assign(reqCtx, hookResult);
9891
9927
  }
9892
9928
  const route = resolveRouteMiddleware(path);
9893
9929
  const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
@@ -10030,9 +10066,9 @@ ${html}`;
10030
10066
  if (path === "/index")
10031
10067
  path = "/";
10032
10068
  if (path === "/") {
10033
- const indexContent = await getRoute("/");
10069
+ const indexContent = await getRoute("/", reqCtx);
10034
10070
  if (!indexContent) {
10035
- const homeContent = await getRoute("/home");
10071
+ const homeContent = await getRoute("/home", reqCtx);
10036
10072
  if (homeContent) {
10037
10073
  return new Response(null, {
10038
10074
  status: 302,
@@ -10044,7 +10080,7 @@ ${html}`;
10044
10080
  }
10045
10081
  }
10046
10082
  }
10047
- const content = await getRoute(path);
10083
+ const content = await getRoute(path, reqCtx);
10048
10084
  if (content) {
10049
10085
  const isSpaNav = req.headers.get("X-STX-Router") === "true";
10050
10086
  if (isSpaNav) {
@@ -10140,7 +10176,7 @@ ${pageSetupScripts.join(`
10140
10176
  }
10141
10177
  });
10142
10178
  }
10143
- const contentWithExt = await getRoute(`${path}.html`);
10179
+ const contentWithExt = await getRoute(`${path}.html`, reqCtx);
10144
10180
  if (contentWithExt) {
10145
10181
  return new Response(injectHmrClient(contentWithExt), {
10146
10182
  headers: {
@@ -10231,7 +10267,7 @@ ${pageSetupScripts.join(`
10231
10267
  const isProd = isProductionServe();
10232
10268
  for (const custom404 of ["/404", "/errors/404"]) {
10233
10269
  try {
10234
- const customContent = await getRoute(custom404);
10270
+ const customContent = await getRoute(custom404, reqCtx);
10235
10271
  if (customContent) {
10236
10272
  return new Response(isProd ? customContent : injectHmrClient(customContent), {
10237
10273
  status: 404,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.84",
4
+ "version": "0.2.86",
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",