bun-plugin-stx 0.2.309 → 0.2.310

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.
@@ -1468,8 +1468,10 @@ else {
1468
1468
  // Before the swap: add each sheet the page does not have yet, as
1469
1469
  // media="print" so it downloads without restyling the page still on screen.
1470
1470
  // Returns a promise for them to finish (load OR error, capped at
1471
- // cssLoadTimeout so a slow or missing sheet never stalls navigation), or null
1472
- // when nothing new is needed and the swap can go ahead in the same turn.
1471
+ // cssLoadTimeout so a slow sheet never stalls navigation), or null when
1472
+ // nothing new is needed and the swap can go ahead in the same turn. The
1473
+ // promise resolves false when a sheet failed outright, and the caller then
1474
+ // navigates in full rather than swap in markup that has no styles.
1473
1475
  function preloadGeneratedCss(hrefs,base){
1474
1476
  var pending=[];
1475
1477
  hrefs.forEach(function(href){
@@ -1490,14 +1492,34 @@ else {
1490
1492
  link.setAttribute('data-stx-css-pending','');
1491
1493
  link.__stxCssReady=new Promise(function(resolve){
1492
1494
  var timer=setTimeout(finish,o.cssLoadTimeout);
1493
- function finish(){clearTimeout(timer);link.onload=null;link.onerror=null;link.__stxCssReady=null;resolve()}
1494
- link.onload=finish;
1495
- link.onerror=finish;
1495
+ function finish(failed){
1496
+ clearTimeout(timer);link.onload=null;link.onerror=null;link.__stxCssReady=null;
1497
+ // A sheet that failed (a 5xx, a dropped connection) is gone for good:
1498
+ // swapping anyway painted the page with only the utilities the
1499
+ // previous page happened to share, which reads as a broken layout.
1500
+ // Drop the dead <link> so a retry fetches it again, and report it.
1501
+ if(failed===true&&link.parentNode)link.parentNode.removeChild(link);
1502
+ resolve(failed!==true);
1503
+ }
1504
+ link.onload=function(){finish(false)};
1505
+ link.onerror=function(){finish(true)};
1496
1506
  });
1497
1507
  pending.push(link.__stxCssReady);
1498
1508
  document.head.appendChild(link);
1499
1509
  });
1500
- return pending.length?Promise.all(pending):null;
1510
+ return pending.length?Promise.all(pending).then(function(results){return results.every(function(ok){return ok!==false})}):null;
1511
+ }
1512
+
1513
+ // Swap once the destination's sheets are in, or hand the navigation to the
1514
+ // browser when one could not be fetched \u2014 a full load retries the sheet.
1515
+ function afterCss(ready,url,swapNow){
1516
+ if(!ready)return swapNow();
1517
+ return ready.then(function(ok){
1518
+ if(ok)return swapNow();
1519
+ log('[router] stylesheet failed to load \u2014 full navigation to:',url);
1520
+ location.href=url;
1521
+ return false;
1522
+ });
1501
1523
  }
1502
1524
 
1503
1525
  // At the swap: make the destination's sheets live and move them to the end
@@ -1772,7 +1794,7 @@ else {
1772
1794
  // jumps. Cleanup waits with it so the outgoing page stays live meanwhile.
1773
1795
  // With nothing new to load this runs in the same turn, as it always did.
1774
1796
  var fragCssReady=preloadGeneratedCss(fragmentCssHrefs(html),location.href);
1775
- return fragCssReady?fragCssReady.then(startFragSwap):startFragSwap();
1797
+ return afterCss(fragCssReady,url,startFragSwap);
1776
1798
  }
1777
1799
 
1778
1800
  // Full document mode: parse with DOMParser and extract container content
@@ -2214,7 +2236,7 @@ else {
2214
2236
  });
2215
2237
  }
2216
2238
  var docCssReady=preloadGeneratedCss(docCssHrefs,docCssBase);
2217
- return docCssReady?docCssReady.then(startSwap):startSwap();
2239
+ return afterCss(docCssReady,url,startSwap);
2218
2240
  }
2219
2241
 
2220
2242
  // \u2500\u2500 Link interception \u2500\u2500
package/dist/serve.d.ts CHANGED
@@ -11,6 +11,18 @@ export declare function extractServerScriptsFromTemplate(content: string): {
11
11
  serverScripts: string[]
12
12
  templateContent: string
13
13
  };
14
+ /** The params as a script-safe object literal. */
15
+ export declare function serializeRouteParams(params: Record<string, string>): string;
16
+ /** Put one request's params into a cached dynamic-route shell. */
17
+ export declare function fillRouteParams(html: string, params: Record<string, string>): string;
18
+ /**
19
+ * Whether a page's server scripts read its route params — `params`, or a
20
+ * segment by its bare name (`[id].stx` binds `id`). Such a page renders per
21
+ * record and must not share one cached render across them. Deliberately
22
+ * coarse: a comment or an unrelated local spelled the same only costs the
23
+ * cache, never correctness.
24
+ */
25
+ export declare function serverScriptsReadParams(serverScripts: string[], paramNames: string[]): boolean;
14
26
  /**
15
27
  * A token to mint for this request, or null to leave it alone.
16
28
  *
@@ -199,6 +211,12 @@ export declare function serve(options: ServeOptions): Promise<void>;
199
211
  * own templates, including a framework's templates vendored into it.
200
212
  */
201
213
  export declare function projectUsesImageBuiltins(root: string): Promise<boolean>;
214
+ /**
215
+ * Stands in for a dynamic route's params in a cached shell. Not valid
216
+ * JavaScript on its own, so a shell that escaped without being filled fails
217
+ * loudly instead of running with somebody else's params.
218
+ */
219
+ export declare const ROUTE_PARAMS_PLACEHOLDER: '__STX_ROUTE_PARAMS__';
202
220
  /**
203
221
  * The cookie name a double-submit CSRF check reads.
204
222
  *
package/dist/serve.js CHANGED
@@ -8851,6 +8851,19 @@ function extractServerScriptsFromTemplate(content) {
8851
8851
  retained.push(content.slice(retainedFrom));
8852
8852
  return { serverScripts, templateContent: retained.join("") };
8853
8853
  }
8854
+ var ROUTE_PARAMS_PLACEHOLDER = "__STX_ROUTE_PARAMS__";
8855
+ function serializeRouteParams(params) {
8856
+ return JSON.stringify(params).replace(/</g, "\\u003C").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
8857
+ }
8858
+ function fillRouteParams(html, params) {
8859
+ const serialized = serializeRouteParams(params);
8860
+ return html.replace(ROUTE_PARAMS_PLACEHOLDER, () => serialized);
8861
+ }
8862
+ function serverScriptsReadParams(serverScripts, paramNames) {
8863
+ const names = ["params", ...paramNames].map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
8864
+ const pattern = new RegExp(`(?:^|[^\\w$.])(?:${names.join("|")})(?![\\w$])`);
8865
+ return serverScripts.some((script) => pattern.test(script));
8866
+ }
8854
8867
  var CSRF_COOKIE = "X-CSRF-Token";
8855
8868
  function csrfTokenToMint(req, cookies) {
8856
8869
  const method = (req.method || "GET").toUpperCase();
@@ -9713,7 +9726,7 @@ function __stxOverlay(errs){
9713
9726
  pageMiddlewarePatterns.length = 0;
9714
9727
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9715
9728
  try {
9716
- const { Router } = await import("./chunk-ahmasn2t.js");
9729
+ const { Router } = await import("./chunk-zt4e86p1.js");
9717
9730
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9718
9731
  const router = new Router(process19.cwd(), { pagesDirs, stateDir: stateDirName() });
9719
9732
  if (!options.quiet)
@@ -10114,12 +10127,6 @@ function __stxOverlay(errs){
10114
10127
  if (reqCtx)
10115
10128
  reqCtx.responseStatus = extractPageResponseStatus(content) ?? 200;
10116
10129
  const { serverScripts: dynServerScripts, templateContent } = extractServerScriptsFromTemplate(content);
10117
- const context = {
10118
- __filename: filePath,
10119
- __dirname: nodePath.dirname(filePath),
10120
- __route: routePath,
10121
- __stx_runtime_head: {}
10122
- };
10123
10130
  const paramsObj = {};
10124
10131
  for (let i = 0;i < paramNames.length; i++) {
10125
10132
  let value = paramValues[i] ?? "";
@@ -10128,12 +10135,31 @@ function __stxOverlay(errs){
10128
10135
  } catch {}
10129
10136
  paramsObj[paramNames[i]] = value;
10130
10137
  }
10138
+ const isMutating = !!reqCtx && reqCtx.method !== "GET" && reqCtx.method !== "HEAD";
10139
+ const skipCacheHint = /(?:^|[^\w$])__stx_skip_cache\s*=\s*true/.test(content);
10140
+ const cacheShell = ENABLE_HTML_CACHE && RENDER_CACHE_VARY === "source" && paramNames.length > 0 && !isMutating && !skipCacheHint && !serverScriptsReadParams(dynServerScripts, paramNames);
10141
+ if (reqCtx)
10142
+ reqCtx.params = paramsObj;
10143
+ if (cacheShell) {
10144
+ const cachedEntry = htmlCache.get(filePath);
10145
+ if (cachedEntry && await templateSignatureFresh(cachedEntry.signature)) {
10146
+ if (reqCtx) {
10147
+ reqCtx.responseStatus = cachedEntry.status;
10148
+ reqCtx.responseHeaders = cachedEntry.headers;
10149
+ }
10150
+ return fillRouteParams(cachedEntry.html, paramsObj);
10151
+ }
10152
+ }
10153
+ const context = {
10154
+ __filename: filePath,
10155
+ __dirname: nodePath.dirname(filePath),
10156
+ __route: routePath,
10157
+ __stx_runtime_head: {}
10158
+ };
10131
10159
  for (const name of paramNames) {
10132
10160
  context[name] = paramsObj[name];
10133
10161
  }
10134
10162
  context.params = paramsObj;
10135
- if (reqCtx)
10136
- reqCtx.params = paramsObj;
10137
10163
  const { processDirectives, extractVariables, defaultConfig: defaultConfig4, injectRouterScript: injectRouter } = await stxModule;
10138
10164
  injectServeLocaleContext(context);
10139
10165
  injectServeRequestContext(context, reqCtx);
@@ -10172,7 +10198,7 @@ function __stxOverlay(errs){
10172
10198
  output = await processDirectives(output, context, filePath, config5, dependencies);
10173
10199
  applyRecordedResponse(context, reqCtx);
10174
10200
  if (paramNames.length > 0) {
10175
- const serializedParams = JSON.stringify(paramsObj).replace(/</g, "\\u003C").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
10201
+ const serializedParams = cacheShell ? ROUTE_PARAMS_PLACEHOLDER : serializeRouteParams(paramsObj);
10176
10202
  const paramsScript = `<script data-stx-route-params>(function(){var p=${serializedParams};window.__stx_rp=p;if(window.stx){window.stx._rp=p;if(window.stx.setRouteParams)window.stx.setRouteParams(p)}})()</script>`;
10177
10203
  if (output.includes("</head>")) {
10178
10204
  output = output.replace("</head>", `${paramsScript}
@@ -10191,7 +10217,14 @@ ${output}`;
10191
10217
  return match;
10192
10218
  return "";
10193
10219
  });
10194
- return output;
10220
+ if (!cacheShell)
10221
+ return output;
10222
+ const recordedResponse = readResponseStatus(context) !== undefined || !!readResponseHeaders(context);
10223
+ if (!context.__stx_skip_cache && !recordedResponse && isRenderableCacheCandidate(output) && placeholdersAreReady) {
10224
+ const signature = await buildTemplateSignature(filePath, dependencies);
10225
+ htmlCache.set(filePath, { html: output, signature, status: reqCtx?.responseStatus ?? 200, headers: reqCtx?.responseHeaders });
10226
+ }
10227
+ return fillRouteParams(output, paramsObj);
10195
10228
  }
10196
10229
  const startTime = performance.now();
10197
10230
  await discoverFiles();
@@ -11148,7 +11181,9 @@ export {
11148
11181
  usableErrorPage,
11149
11182
  staticCacheControl,
11150
11183
  sharedScriptCacheControl,
11184
+ serverScriptsReadParams,
11151
11185
  serve,
11186
+ serializeRouteParams,
11152
11187
  routeSpecificity,
11153
11188
  responseTakesCsrfCookie,
11154
11189
  resolveServeOpenUrl,
@@ -11159,6 +11194,7 @@ export {
11159
11194
  isRenderableCacheCandidate,
11160
11195
  isRenderFailure,
11161
11196
  isProductionServe,
11197
+ fillRouteParams,
11162
11198
  extractServerScriptsFromTemplate,
11163
11199
  csrfTokenToMint,
11164
11200
  csrfCookieHeader,
@@ -11166,5 +11202,6 @@ export {
11166
11202
  buildDynamicRouteRegexes,
11167
11203
  boundedCache,
11168
11204
  SHARED_SCRIPT_PATH,
11205
+ ROUTE_PARAMS_PLACEHOLDER,
11169
11206
  CSRF_COOKIE
11170
11207
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.309",
4
+ "version": "0.2.310",
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",