bun-plugin-stx 0.2.308 → 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
  *
@@ -191,6 +203,20 @@ export declare function bundleBrowserAsset(entrypoint: string): Promise<Response
191
203
  * @param options Server options with patterns and port
192
204
  */
193
205
  export declare function serve(options: ServeOptions): Promise<void>;
206
+ /**
207
+ * Whether any `.stx` template under `root` uses `<StxImage>` or `@image`.
208
+ *
209
+ * Stops at the first hit. Skips dependencies, build output and hidden
210
+ * directories (the stx state directory among them), so it reads the project's
211
+ * own templates, including a framework's templates vendored into it.
212
+ */
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__';
194
220
  /**
195
221
  * The cookie name a double-submit CSRF check reads.
196
222
  *
@@ -258,7 +284,7 @@ export declare interface ServeOptions {
258
284
  cssCacheLimit?: number
259
285
  prewarmRenderCache?: boolean | number
260
286
  imageWarmupGraceMs?: number
261
- imageWarmup?: boolean
287
+ imageWarmup?: boolean | 'auto'
262
288
  publicDir?: string
263
289
  quiet?: boolean
264
290
  autoIncrementPort?: boolean | number
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();
@@ -9216,7 +9229,7 @@ async function serve(options) {
9216
9229
  markPlaceholdersReady = resolve13;
9217
9230
  });
9218
9231
  let placeholdersAreReady = false;
9219
- const imageWarmupEnabled = (options.imageWarmup ?? stxConfig.imageWarmup ?? true) !== false;
9232
+ const imageWarmupSetting = options.imageWarmup ?? stxConfig.imageWarmup ?? "auto";
9220
9233
  const imageWarmupGraceMs = Math.max(0, options.imageWarmupGraceMs ?? 1000);
9221
9234
  function awaitImageWarmup() {
9222
9235
  if (placeholdersAreReady || imageWarmupGraceMs === 0)
@@ -9231,7 +9244,9 @@ async function serve(options) {
9231
9244
  });
9232
9245
  }
9233
9246
  const stxModule = options.stxModule ? Promise.resolve(options.stxModule) : defaultStxModule;
9234
- const imageDeliveryOutputDir = stateDir(process19.cwd(), "image-delivery");
9247
+ const imageCacheDir = process19.env.STX_IMAGE_CACHE_DIR?.trim();
9248
+ const imageDeliveryOutputDir = imageCacheDir ? nodePath.resolve(imageCacheDir, "image-delivery") : stateDir(process19.cwd(), "image-delivery");
9249
+ const imagePlaceholderCachePath = imageCacheDir ? nodePath.resolve(imageCacheDir, "image-placeholders.json") : stateDir(process19.cwd(), "image-placeholders.json");
9235
9250
  const imageDeliveryRoot = nodePath.join(imageDeliveryOutputDir, "_stx", "images");
9236
9251
  let resolvedStxModule = null;
9237
9252
  Promise.resolve(stxModule).then((m) => {
@@ -9711,7 +9726,7 @@ function __stxOverlay(errs){
9711
9726
  pageMiddlewarePatterns.length = 0;
9712
9727
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
9713
9728
  try {
9714
- const { Router } = await import("./chunk-ahmasn2t.js");
9729
+ const { Router } = await import("./chunk-zt4e86p1.js");
9715
9730
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
9716
9731
  const router = new Router(process19.cwd(), { pagesDirs, stateDir: stateDirName() });
9717
9732
  if (!options.quiet)
@@ -10112,12 +10127,6 @@ function __stxOverlay(errs){
10112
10127
  if (reqCtx)
10113
10128
  reqCtx.responseStatus = extractPageResponseStatus(content) ?? 200;
10114
10129
  const { serverScripts: dynServerScripts, templateContent } = extractServerScriptsFromTemplate(content);
10115
- const context = {
10116
- __filename: filePath,
10117
- __dirname: nodePath.dirname(filePath),
10118
- __route: routePath,
10119
- __stx_runtime_head: {}
10120
- };
10121
10130
  const paramsObj = {};
10122
10131
  for (let i = 0;i < paramNames.length; i++) {
10123
10132
  let value = paramValues[i] ?? "";
@@ -10126,12 +10135,31 @@ function __stxOverlay(errs){
10126
10135
  } catch {}
10127
10136
  paramsObj[paramNames[i]] = value;
10128
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
+ };
10129
10159
  for (const name of paramNames) {
10130
10160
  context[name] = paramsObj[name];
10131
10161
  }
10132
10162
  context.params = paramsObj;
10133
- if (reqCtx)
10134
- reqCtx.params = paramsObj;
10135
10163
  const { processDirectives, extractVariables, defaultConfig: defaultConfig4, injectRouterScript: injectRouter } = await stxModule;
10136
10164
  injectServeLocaleContext(context);
10137
10165
  injectServeRequestContext(context, reqCtx);
@@ -10170,7 +10198,7 @@ function __stxOverlay(errs){
10170
10198
  output = await processDirectives(output, context, filePath, config5, dependencies);
10171
10199
  applyRecordedResponse(context, reqCtx);
10172
10200
  if (paramNames.length > 0) {
10173
- 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);
10174
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>`;
10175
10203
  if (output.includes("</head>")) {
10176
10204
  output = output.replace("</head>", `${paramsScript}
@@ -10189,7 +10217,14 @@ ${output}`;
10189
10217
  return match;
10190
10218
  return "";
10191
10219
  });
10192
- 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);
10193
10228
  }
10194
10229
  const startTime = performance.now();
10195
10230
  await discoverFiles();
@@ -10292,6 +10327,7 @@ ${block}`;
10292
10327
  });
10293
10328
  }
10294
10329
  async function runImageWarmup() {
10330
+ const imageWarmupEnabled = imageWarmupSetting === "auto" ? await projectUsesImageBuiltins(process19.cwd()) : imageWarmupSetting !== false;
10295
10331
  if (!imageWarmupEnabled) {
10296
10332
  placeholdersAreReady = true;
10297
10333
  markPlaceholdersReady();
@@ -10302,7 +10338,7 @@ ${block}`;
10302
10338
  const publicRoot = nodePath.resolve(process19.cwd(), publicDir);
10303
10339
  const [placeholderResult, deliveryResult] = await Promise.allSettled([
10304
10340
  stx.warmImagePlaceholders(publicRoot, {
10305
- cachePath: stateDir(process19.cwd(), "image-placeholders.json")
10341
+ cachePath: imagePlaceholderCachePath
10306
10342
  }),
10307
10343
  stx.prepareImageDelivery(publicRoot, imageDeliveryOutputDir)
10308
10344
  ]);
@@ -11109,20 +11145,56 @@ async function main() {
11109
11145
  if (import.meta.main) {
11110
11146
  main().catch(console.error);
11111
11147
  }
11148
+ var IMAGE_BUILTIN_PATTERN = /<(?:StxImage|Image|stx-image|stx-img)[\s/>]|@image\s*\(/;
11149
+ var TEMPLATE_SCAN_SKIP = new Set(["node_modules", "dist", "build", "coverage", "vendor"]);
11150
+ async function projectUsesImageBuiltins(root) {
11151
+ const { readdir: readdir4, readFile } = await import("fs/promises");
11152
+ const pending = [root];
11153
+ while (pending.length > 0) {
11154
+ const dir = pending.pop();
11155
+ let entries;
11156
+ try {
11157
+ entries = await readdir4(dir, { withFileTypes: true });
11158
+ } catch {
11159
+ continue;
11160
+ }
11161
+ for (const entry of entries) {
11162
+ if (entry.name.startsWith("."))
11163
+ continue;
11164
+ const full = nodePath.join(dir, entry.name);
11165
+ if (entry.isDirectory()) {
11166
+ if (!TEMPLATE_SCAN_SKIP.has(entry.name))
11167
+ pending.push(full);
11168
+ continue;
11169
+ }
11170
+ if (!entry.isFile() || !entry.name.endsWith(".stx"))
11171
+ continue;
11172
+ try {
11173
+ if (IMAGE_BUILTIN_PATTERN.test(await readFile(full, "utf8")))
11174
+ return true;
11175
+ } catch {}
11176
+ }
11177
+ }
11178
+ return false;
11179
+ }
11112
11180
  export {
11113
11181
  usableErrorPage,
11114
11182
  staticCacheControl,
11115
11183
  sharedScriptCacheControl,
11184
+ serverScriptsReadParams,
11116
11185
  serve,
11186
+ serializeRouteParams,
11117
11187
  routeSpecificity,
11118
11188
  responseTakesCsrfCookie,
11119
11189
  resolveServeOpenUrl,
11120
11190
  render404Page,
11121
11191
  publicFileExists,
11192
+ projectUsesImageBuiltins,
11122
11193
  isStaticAssetPath,
11123
11194
  isRenderableCacheCandidate,
11124
11195
  isRenderFailure,
11125
11196
  isProductionServe,
11197
+ fillRouteParams,
11126
11198
  extractServerScriptsFromTemplate,
11127
11199
  csrfTokenToMint,
11128
11200
  csrfCookieHeader,
@@ -11130,5 +11202,6 @@ export {
11130
11202
  buildDynamicRouteRegexes,
11131
11203
  boundedCache,
11132
11204
  SHARED_SCRIPT_PATH,
11205
+ ROUTE_PARAMS_PLACEHOLDER,
11133
11206
  CSRF_COOKIE
11134
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.308",
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",