bun-plugin-stx 0.2.616 → 0.2.617

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.
package/dist/serve.js CHANGED
@@ -9390,6 +9390,20 @@ function resolveImageWarmupBindBudget(value) {
9390
9390
  const ms = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
9391
9391
  return typeof ms === "number" && Number.isFinite(ms) && ms >= 0 ? ms : DEFAULT_IMAGE_WARMUP_BIND_BUDGET_MS;
9392
9392
  }
9393
+ var PAGE_WARMUP_HEADER = "x-stx-warmup";
9394
+ var DEFAULT_PAGE_WARMUP_BUDGET_MS = 60000;
9395
+ function resolvePageWarmup(value) {
9396
+ const defaults = { enabled: true, paths: [], budgetMs: DEFAULT_PAGE_WARMUP_BUDGET_MS, concurrency: 4 };
9397
+ if (value === false)
9398
+ return { ...defaults, enabled: false };
9399
+ if (!value || typeof value !== "object")
9400
+ return defaults;
9401
+ const raw = value;
9402
+ const paths = Array.isArray(raw.paths) ? [...new Set(raw.paths.filter((p) => typeof p === "string" && p.startsWith("/") && !p.startsWith("//")))] : [];
9403
+ const budgetMs = typeof raw.budgetMs === "number" && Number.isFinite(raw.budgetMs) && raw.budgetMs >= 0 ? raw.budgetMs : defaults.budgetMs;
9404
+ const concurrency = typeof raw.concurrency === "number" && Number.isFinite(raw.concurrency) && raw.concurrency >= 1 ? Math.floor(raw.concurrency) : defaults.concurrency;
9405
+ return { enabled: true, paths, budgetMs, concurrency };
9406
+ }
9393
9407
  async function settleWithin(promise, ms) {
9394
9408
  let timer;
9395
9409
  const settled = promise.then(() => true, () => true);
@@ -9455,6 +9469,7 @@ async function serve(options) {
9455
9469
  const imageWarmupSetting = options.imageWarmup ?? stxConfig.imageWarmup ?? "auto";
9456
9470
  const imageWarmupGraceMs = Math.max(0, options.imageWarmupGraceMs ?? 1000);
9457
9471
  const imageWarmupBindBudgetMs = resolveImageWarmupBindBudget(options.imageWarmupBindBudgetMs ?? stxConfig.imageWarmupBindBudgetMs);
9472
+ const pageWarmup = resolvePageWarmup(options.pageWarmup ?? stxConfig.pageWarmup);
9458
9473
  function awaitImageWarmup() {
9459
9474
  if (placeholdersAreReady || imageWarmupGraceMs === 0)
9460
9475
  return;
@@ -10639,537 +10654,532 @@ ${block}`;
10639
10654
  });
10640
10655
  }
10641
10656
  }
10642
- const portAutoIncrement = options.autoIncrementPort ?? true;
10643
- const maxPortAttempts = portAutoIncrement === false ? 1 : typeof portAutoIncrement === "number" ? Math.max(1, portAutoIncrement) : 10;
10644
- let actualPort = port;
10645
- let _server;
10646
- let lastServeError;
10647
- for (let attempt = 0;attempt < maxPortAttempts; attempt++) {
10648
- try {
10649
- _server = bunServe({
10650
- port: actualPort,
10651
- reusePort: options.reusePort ?? false,
10652
- idleTimeout: production ? 120 : 0,
10653
- error(error) {
10654
- console.error("[stx] Request failed:", error);
10655
- return new Response("<h1>Internal Server Error</h1><p>An error occurred while processing your request.</p>", {
10656
- status: 500,
10657
- headers: {
10658
- "Content-Type": "text/html; charset=utf-8",
10659
- "Cache-Control": "no-store"
10660
- }
10661
- });
10662
- },
10663
- async fetch(req, server) {
10664
- const warmup = awaitImageWarmup();
10665
- if (warmup)
10666
- await warmup;
10667
- return compressResponse(req, await (async () => {
10668
- if (domainRoutes.length) {
10669
- const original = new URL(req.url);
10670
- const routed = resolveDomainRoute(requestHost(req), original.pathname, domainRoutes);
10671
- if (routed) {
10672
- original.pathname = routed;
10673
- const headers = new Headers(req.headers);
10674
- headers.set("x-stx-domain-route", requestHost(req));
10675
- req = new Request(original, { headers, method: req.method, body: req.body, redirect: req.redirect, duplex: "half" });
10676
- } else if (req.method === "GET" || req.method === "HEAD") {
10677
- const staysHere = (pathname) => {
10678
- return pathname.startsWith("/api/") || pathname.startsWith("/_stx/") || pathname.startsWith("/.well-known/") || isStaticAssetPath(pathname) || publicFileExists(pathname, publicDir);
10679
- };
10680
- const location = resolveDomainElsewhere(requestHost(req), original.pathname, original.search, domainRoutes, staysHere);
10681
- if (location) {
10682
- if (req.headers.get(SPA_NAV_HEADER) === "true")
10683
- return new Response(null, { status: 200, headers: { "X-STX-Location": location, "Cache-Control": "no-store", Vary: SPA_NAV_HEADER } });
10684
- return new Response(null, { status: 301, headers: { Location: location } });
10685
- }
10657
+ function listen(listenPort, reusePort, hostname) {
10658
+ return bunServe({
10659
+ port: listenPort,
10660
+ ...hostname ? { hostname } : {},
10661
+ reusePort,
10662
+ idleTimeout: production ? 120 : 0,
10663
+ error(error) {
10664
+ console.error("[stx] Request failed:", error);
10665
+ return new Response("<h1>Internal Server Error</h1><p>An error occurred while processing your request.</p>", {
10666
+ status: 500,
10667
+ headers: {
10668
+ "Content-Type": "text/html; charset=utf-8",
10669
+ "Cache-Control": "no-store"
10670
+ }
10671
+ });
10672
+ },
10673
+ async fetch(req, server) {
10674
+ const warmup = awaitImageWarmup();
10675
+ if (warmup)
10676
+ await warmup;
10677
+ return compressResponse(req, await (async () => {
10678
+ if (domainRoutes.length) {
10679
+ const original = new URL(req.url);
10680
+ const routed = resolveDomainRoute(requestHost(req), original.pathname, domainRoutes);
10681
+ if (routed) {
10682
+ original.pathname = routed;
10683
+ const headers = new Headers(req.headers);
10684
+ headers.set("x-stx-domain-route", requestHost(req));
10685
+ req = new Request(original, { headers, method: req.method, body: req.body, redirect: req.redirect, duplex: "half" });
10686
+ } else if (req.method === "GET" || req.method === "HEAD") {
10687
+ const staysHere = (pathname) => {
10688
+ return pathname.startsWith("/api/") || pathname.startsWith("/_stx/") || pathname.startsWith("/.well-known/") || isStaticAssetPath(pathname) || publicFileExists(pathname, publicDir);
10689
+ };
10690
+ const location = resolveDomainElsewhere(requestHost(req), original.pathname, original.search, domainRoutes, staysHere);
10691
+ if (location) {
10692
+ if (req.headers.get(SPA_NAV_HEADER) === "true")
10693
+ return new Response(null, { status: 200, headers: { "X-STX-Location": location, "Cache-Control": "no-store", Vary: SPA_NAV_HEADER } });
10694
+ return new Response(null, { status: 301, headers: { Location: location } });
10686
10695
  }
10687
10696
  }
10688
- const url = new URL(req.url);
10689
- let path = url.pathname;
10690
- const proxyRules = stxConfig.server?.proxy;
10691
- if (proxyRules) {
10692
- for (const [prefix, rule] of Object.entries(proxyRules)) {
10693
- if (path !== prefix && !path.startsWith(prefix))
10694
- continue;
10695
- const ruleCfg = typeof rule === "string" ? { target: rule } : rule;
10696
- const outPath = ruleCfg.rewrite ? ruleCfg.rewrite(path) : path;
10697
- const target = new URL(ruleCfg.target);
10698
- const outUrl = new URL(outPath + url.search, target);
10699
- const headers = new Headers(req.headers);
10700
- if (ruleCfg.changeOrigin !== false)
10701
- headers.set("host", target.host);
10702
- try {
10703
- return await fetch(outUrl, {
10704
- method: req.method,
10705
- headers,
10706
- body: req.body,
10707
- redirect: "manual",
10708
- ...req.body ? { duplex: "half" } : {}
10709
- });
10710
- } catch (err) {
10711
- return new Response(`[stx dev proxy] ${prefix} \u2192 ${ruleCfg.target} failed: ${err instanceof Error ? err.message : String(err)}`, { status: 502 });
10712
- }
10697
+ }
10698
+ const url = new URL(req.url);
10699
+ let path = url.pathname;
10700
+ const proxyRules = stxConfig.server?.proxy;
10701
+ if (proxyRules) {
10702
+ for (const [prefix, rule] of Object.entries(proxyRules)) {
10703
+ if (path !== prefix && !path.startsWith(prefix))
10704
+ continue;
10705
+ const ruleCfg = typeof rule === "string" ? { target: rule } : rule;
10706
+ const outPath = ruleCfg.rewrite ? ruleCfg.rewrite(path) : path;
10707
+ const target = new URL(ruleCfg.target);
10708
+ const outUrl = new URL(outPath + url.search, target);
10709
+ const headers = new Headers(req.headers);
10710
+ if (ruleCfg.changeOrigin !== false)
10711
+ headers.set("host", target.host);
10712
+ try {
10713
+ return await fetch(outUrl, {
10714
+ method: req.method,
10715
+ headers,
10716
+ body: req.body,
10717
+ redirect: "manual",
10718
+ ...req.body ? { duplex: "half" } : {}
10719
+ });
10720
+ } catch (err) {
10721
+ return new Response(`[stx dev proxy] ${prefix} \u2192 ${ruleCfg.target} failed: ${err instanceof Error ? err.message : String(err)}`, { status: 502 });
10713
10722
  }
10714
10723
  }
10715
- let i18nLocale = null;
10716
- if (i18nConfig && !url.pathname.startsWith("/api/")) {
10717
- const stripped = localeFromPath(url.pathname);
10718
- i18nLocale = stripped.locale;
10719
- if (stripped.path !== url.pathname) {
10720
- const rewritten = new URL(req.url);
10721
- rewritten.pathname = stripped.path;
10722
- const headers = new Headers(req.headers);
10723
- headers.set("x-stx-locale", stripped.locale);
10724
- req = new Request(rewritten, { headers, method: req.method, body: req.body, redirect: req.redirect, duplex: "half" });
10725
- path = stripped.path;
10726
- } else if (i18nLocale) {
10727
- const headers = new Headers(req.headers);
10728
- headers.set("x-stx-locale", i18nLocale);
10729
- req = new Request(req, { headers });
10730
- }
10724
+ }
10725
+ let i18nLocale = null;
10726
+ if (i18nConfig && !url.pathname.startsWith("/api/")) {
10727
+ const stripped = localeFromPath(url.pathname);
10728
+ i18nLocale = stripped.locale;
10729
+ if (stripped.path !== url.pathname) {
10730
+ const rewritten = new URL(req.url);
10731
+ rewritten.pathname = stripped.path;
10732
+ const headers = new Headers(req.headers);
10733
+ headers.set("x-stx-locale", stripped.locale);
10734
+ req = new Request(rewritten, { headers, method: req.method, body: req.body, redirect: req.redirect, duplex: "half" });
10735
+ path = stripped.path;
10736
+ } else if (i18nLocale) {
10737
+ const headers = new Headers(req.headers);
10738
+ headers.set("x-stx-locale", i18nLocale);
10739
+ req = new Request(req, { headers });
10731
10740
  }
10732
- let mintedCsrfToken = null;
10733
- const _i18nResp = await (async () => {
10734
- activeServeLocale = i18nLocale;
10735
- activeServeSearch = url.search;
10736
- activeServeHost = req.headers.get("host") || "";
10737
- activeServeCookieHeader = req.headers.get("cookie") || "";
10738
- activeServeCookies = parseCookies(req);
10739
- activeServeIp = resolveClientAddress(req, server);
10740
- mintedCsrfToken = csrfTokenToMint(req, activeServeCookies);
10741
- if (mintedCsrfToken)
10742
- activeServeCookies[CSRF_COOKIE] = mintedCsrfToken;
10743
- const reqCtx = {
10744
- url: req.url,
10745
- path,
10746
- search: activeServeSearch,
10747
- host: activeServeHost,
10748
- cookieHeader: activeServeCookieHeader,
10749
- cookies: activeServeCookies,
10750
- ip: activeServeIp,
10751
- locale: activeServeLocale,
10752
- params: {},
10753
- method: (req.method || "GET").toUpperCase(),
10754
- request: req
10755
- };
10756
- try {
10757
- if (req.method === "OPTIONS") {
10758
- return new Response(null, { headers: corsHeaders });
10759
- }
10760
- if (path === "/_stx/hmr") {
10761
- if (production)
10762
- return new Response("Not Found", { status: 404, headers: { "Cache-Control": "no-store" } });
10763
- let controller;
10764
- let keepalive;
10765
- const stream = new ReadableStream({
10766
- start(c) {
10767
- controller = c;
10768
- hmrClients.add(c);
10769
- c.enqueue(hmrEncoder.encode(`data: {"type":"connected","version":${hmrVersion}}
10741
+ }
10742
+ let mintedCsrfToken = null;
10743
+ const _i18nResp = await (async () => {
10744
+ activeServeLocale = i18nLocale;
10745
+ activeServeSearch = url.search;
10746
+ activeServeHost = req.headers.get("host") || "";
10747
+ activeServeCookieHeader = req.headers.get("cookie") || "";
10748
+ activeServeCookies = parseCookies(req);
10749
+ activeServeIp = resolveClientAddress(req, server);
10750
+ mintedCsrfToken = csrfTokenToMint(req, activeServeCookies);
10751
+ if (mintedCsrfToken)
10752
+ activeServeCookies[CSRF_COOKIE] = mintedCsrfToken;
10753
+ const reqCtx = {
10754
+ url: req.url,
10755
+ path,
10756
+ search: activeServeSearch,
10757
+ host: activeServeHost,
10758
+ cookieHeader: activeServeCookieHeader,
10759
+ cookies: activeServeCookies,
10760
+ ip: activeServeIp,
10761
+ locale: activeServeLocale,
10762
+ params: {},
10763
+ method: (req.method || "GET").toUpperCase(),
10764
+ request: req
10765
+ };
10766
+ try {
10767
+ if (req.method === "OPTIONS") {
10768
+ return new Response(null, { headers: corsHeaders });
10769
+ }
10770
+ if (path === "/_stx/hmr") {
10771
+ if (production)
10772
+ return new Response("Not Found", { status: 404, headers: { "Cache-Control": "no-store" } });
10773
+ let controller;
10774
+ let keepalive;
10775
+ const stream = new ReadableStream({
10776
+ start(c) {
10777
+ controller = c;
10778
+ hmrClients.add(c);
10779
+ c.enqueue(hmrEncoder.encode(`data: {"type":"connected","version":${hmrVersion}}
10770
10780
 
10771
10781
  `));
10772
- if (currentBuildErrors.length > 0) {
10773
- c.enqueue(hmrEncoder.encode(`data: ${JSON.stringify({ type: "build-error", errors: currentBuildErrors })}
10782
+ if (currentBuildErrors.length > 0) {
10783
+ c.enqueue(hmrEncoder.encode(`data: ${JSON.stringify({ type: "build-error", errors: currentBuildErrors })}
10774
10784
 
10775
10785
  `));
10776
- }
10777
- keepalive = setInterval(() => {
10778
- try {
10779
- c.enqueue(hmrEncoder.encode(`: keepalive
10786
+ }
10787
+ keepalive = setInterval(() => {
10788
+ try {
10789
+ c.enqueue(hmrEncoder.encode(`: keepalive
10780
10790
 
10781
10791
  `));
10782
- } catch {}
10783
- }, 5000);
10784
- },
10785
- cancel() {
10786
- if (keepalive)
10787
- clearInterval(keepalive);
10788
- if (controller)
10789
- hmrClients.delete(controller);
10790
- }
10791
- });
10792
- return new Response(stream, {
10793
- headers: {
10794
- "Content-Type": "text/event-stream",
10795
- "Cache-Control": "no-cache, no-transform",
10796
- Connection: "keep-alive",
10797
- "X-Accel-Buffering": "no"
10798
- }
10799
- });
10800
- }
10801
- const moduleBundle = path.match(MODULE_BUNDLE_PATH);
10802
- if (moduleBundle) {
10803
- const stx = await stxModule;
10804
- const content = stx.getServeModuleBundle(moduleBundle[1]);
10805
- if (content === undefined)
10806
- return new Response("/* stx: unknown module bundle */", { status: 404, headers: { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-store", ...corsHeaders } });
10807
- const etag = `"${moduleBundle[1]}"`;
10808
- const headers = {
10809
- "Content-Type": "application/javascript; charset=utf-8",
10810
- "Cache-Control": "public, max-age=31536000, immutable",
10811
- ETag: etag,
10812
- ...corsHeaders
10813
- };
10814
- if (req.headers.get("if-none-match") === etag)
10815
- return new Response(null, { status: 304, headers });
10816
- if (req.method === "HEAD")
10817
- return new Response(null, { headers });
10818
- return new Response(content, { headers });
10792
+ } catch {}
10793
+ }, 5000);
10794
+ },
10795
+ cancel() {
10796
+ if (keepalive)
10797
+ clearInterval(keepalive);
10798
+ if (controller)
10799
+ hmrClients.delete(controller);
10800
+ }
10801
+ });
10802
+ return new Response(stream, {
10803
+ headers: {
10804
+ "Content-Type": "text/event-stream",
10805
+ "Cache-Control": "no-cache, no-transform",
10806
+ Connection: "keep-alive",
10807
+ "X-Accel-Buffering": "no"
10808
+ }
10809
+ });
10810
+ }
10811
+ const moduleBundle = path.match(MODULE_BUNDLE_PATH);
10812
+ if (moduleBundle) {
10813
+ const stx = await stxModule;
10814
+ const content = stx.getServeModuleBundle(moduleBundle[1]);
10815
+ if (content === undefined)
10816
+ return new Response("/* stx: unknown module bundle */", { status: 404, headers: { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-store", ...corsHeaders } });
10817
+ const etag = `"${moduleBundle[1]}"`;
10818
+ const headers = {
10819
+ "Content-Type": "application/javascript; charset=utf-8",
10820
+ "Cache-Control": "public, max-age=31536000, immutable",
10821
+ ETag: etag,
10822
+ ...corsHeaders
10823
+ };
10824
+ if (req.headers.get("if-none-match") === etag)
10825
+ return new Response(null, { status: 304, headers });
10826
+ if (req.method === "HEAD")
10827
+ return new Response(null, { headers });
10828
+ return new Response(content, { headers });
10829
+ }
10830
+ const sharedScript = path.match(SHARED_SCRIPT_PATH);
10831
+ if (sharedScript) {
10832
+ const stx = await stxModule;
10833
+ const asset = await stx.getServeClientAsset(sharedScript[1], sharedScript[1] === "runtime" && stxConfig.debug === true);
10834
+ const content = asset.content;
10835
+ const etag = `"${asset.hash}"`;
10836
+ const headers = {
10837
+ "Content-Type": "application/javascript; charset=utf-8",
10838
+ "Cache-Control": sharedScriptCacheControl(sharedScript[2], asset.hash),
10839
+ ETag: etag,
10840
+ ...corsHeaders
10841
+ };
10842
+ if (req.headers.get("if-none-match") === etag)
10843
+ return new Response(null, { status: 304, headers });
10844
+ if (req.method === "HEAD")
10845
+ return new Response(null, { headers });
10846
+ return new Response(content, { headers });
10847
+ }
10848
+ if ((req.method === "GET" || req.method === "HEAD") && path.startsWith("/_stx/images/")) {
10849
+ let decodedPath;
10850
+ try {
10851
+ decodedPath = decodeURIComponent(path.slice("/_stx/images/".length));
10852
+ } catch {
10853
+ return new Response("Invalid image path", { status: 400 });
10819
10854
  }
10820
- const sharedScript = path.match(SHARED_SCRIPT_PATH);
10821
- if (sharedScript) {
10822
- const stx = await stxModule;
10823
- const asset = await stx.getServeClientAsset(sharedScript[1], sharedScript[1] === "runtime" && stxConfig.debug === true);
10824
- const content = asset.content;
10825
- const etag = `"${asset.hash}"`;
10826
- const headers = {
10855
+ if (!decodedPath || decodedPath.includes("\x00"))
10856
+ return new Response("Invalid image path", { status: 400 });
10857
+ const resolvedPath = nodePath.resolve(imageDeliveryRoot, decodedPath);
10858
+ if (!resolvedPath.startsWith(`${imageDeliveryRoot}${nodePath.sep}`))
10859
+ return new Response("Invalid image path", { status: 400 });
10860
+ try {
10861
+ const file = Bun.file(resolvedPath);
10862
+ if (await file.exists()) {
10863
+ const stat = await file.stat().catch(() => null);
10864
+ if (stat && !stat.isDirectory()) {
10865
+ const headers = {
10866
+ "Content-Type": staticContentType(resolvedPath),
10867
+ "Cache-Control": "public, max-age=31536000, immutable",
10868
+ ...corsHeaders
10869
+ };
10870
+ return req.method === "HEAD" ? new Response(null, { headers }) : new Response(file, { headers });
10871
+ }
10872
+ }
10873
+ } catch {}
10874
+ return new Response("Image not found", { status: 404 });
10875
+ }
10876
+ if (path === "/_stx/sw.js" && stxConfig.offline?.enabled) {
10877
+ const stx = await stxModule;
10878
+ return new Response(stx.generateOfflineWorker(stxConfig.offline, getBuildId()), {
10879
+ headers: {
10880
+ "Content-Type": "text/javascript; charset=utf-8",
10881
+ "Cache-Control": "no-cache",
10882
+ "Service-Worker-Allowed": "/"
10883
+ }
10884
+ });
10885
+ }
10886
+ if (path === "/_stx/stores.js") {
10887
+ const stx = await stxModule;
10888
+ const code = typeof stx.getStoreScript === "function" ? await stx.getStoreScript() : null;
10889
+ return new Response(code ?? "", {
10890
+ status: code === null ? 404 : 200,
10891
+ headers: {
10827
10892
  "Content-Type": "application/javascript; charset=utf-8",
10828
- "Cache-Control": sharedScriptCacheControl(sharedScript[2], asset.hash),
10829
- ETag: etag,
10893
+ "Cache-Control": "no-store",
10830
10894
  ...corsHeaders
10831
- };
10832
- if (req.headers.get("if-none-match") === etag)
10833
- return new Response(null, { status: 304, headers });
10834
- if (req.method === "HEAD")
10835
- return new Response(null, { headers });
10836
- return new Response(content, { headers });
10895
+ }
10896
+ });
10897
+ }
10898
+ const cssAsset = path.match(/^\/_stx\/css\.([a-f0-9]{16})\.css$/);
10899
+ if (cssAsset) {
10900
+ const stx = await stxModule;
10901
+ const content = stx.getCssServeAsset(cssAsset[1]);
10902
+ if (content === undefined)
10903
+ return new Response("Css asset not found", { status: 404 });
10904
+ const headers = {
10905
+ "Content-Type": "text/css; charset=utf-8",
10906
+ "Cache-Control": "public, max-age=31536000, immutable",
10907
+ ...corsHeaders
10908
+ };
10909
+ if (req.method === "HEAD")
10910
+ return new Response(null, { headers });
10911
+ return new Response(content, { headers });
10912
+ }
10913
+ if (options.onRequest) {
10914
+ const hookResult = await options.onRequest(req, server);
10915
+ if (hookResult instanceof Response)
10916
+ return hookResult;
10917
+ if (hookResult && typeof hookResult === "object")
10918
+ Object.assign(reqCtx, hookResult);
10919
+ }
10920
+ const route = resolveRouteMiddleware(path);
10921
+ const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
10922
+ const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
10923
+ if (requested.length > 0) {
10924
+ const expanded = expandMiddlewareNames(requested);
10925
+ const cookies = parseCookies(req);
10926
+ const ctx = {
10927
+ path,
10928
+ url,
10929
+ params: route.params,
10930
+ cookies,
10931
+ redirect: (to, status = 302) => {
10932
+ const sep = to.includes("?") ? "&" : "?";
10933
+ const next = encodeURIComponent(path + (url.search || ""));
10934
+ return new Response(null, {
10935
+ status,
10936
+ headers: { Location: `${to}${sep}next=${next}` }
10937
+ });
10938
+ }
10939
+ };
10940
+ const middlewareResponse = await runPageMiddleware({
10941
+ request: req,
10942
+ context: ctx,
10943
+ entries: expanded,
10944
+ registry: middlewareRegistry,
10945
+ prepareRequest: options.prepareMiddlewareRequest
10946
+ });
10947
+ if (middlewareResponse)
10948
+ return middlewareResponse;
10949
+ }
10950
+ if (options.routes) {
10951
+ const routeHandler = options.routes[path];
10952
+ if (routeHandler) {
10953
+ return routeHandler(req);
10837
10954
  }
10838
- if ((req.method === "GET" || req.method === "HEAD") && path.startsWith("/_stx/images/")) {
10839
- let decodedPath;
10955
+ }
10956
+ if (path.startsWith("/_stx/component/")) {
10957
+ const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
10958
+ if (componentName) {
10840
10959
  try {
10841
- decodedPath = decodeURIComponent(path.slice("/_stx/images/".length));
10842
- } catch {
10843
- return new Response("Invalid image path", { status: 400 });
10960
+ const { processDirectives, defaultConfig } = await stxModule;
10961
+ const componentTemplate = `<${componentName} />`;
10962
+ const componentOpts = {
10963
+ ...defaultConfig,
10964
+ ...componentsDir && { componentsDir },
10965
+ ...layoutsDir && { layoutsDir },
10966
+ ...fallbackLayoutsDir && { fallbackLayoutsDir },
10967
+ ...fallbackComponentsDir && { fallbackComponentsDir },
10968
+ ...partialsDir && { partialsDir },
10969
+ autoShell: false
10970
+ };
10971
+ const html = await processDirectives(componentTemplate, {}, `${componentsDir}/${componentName}.stx`, componentOpts, new Set);
10972
+ return new Response(html, {
10973
+ headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
10974
+ });
10975
+ } catch (e) {
10976
+ return new Response(`<div class="stx-async-error">${e.message}</div>`, {
10977
+ status: 500,
10978
+ headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
10979
+ });
10844
10980
  }
10845
- if (!decodedPath || decodedPath.includes("\x00"))
10846
- return new Response("Invalid image path", { status: 400 });
10847
- const resolvedPath = nodePath.resolve(imageDeliveryRoot, decodedPath);
10848
- if (!resolvedPath.startsWith(`${imageDeliveryRoot}${nodePath.sep}`))
10849
- return new Response("Invalid image path", { status: 400 });
10850
- try {
10851
- const file = Bun.file(resolvedPath);
10852
- if (await file.exists()) {
10853
- const stat = await file.stat().catch(() => null);
10854
- if (stat && !stat.isDirectory()) {
10855
- const headers = {
10856
- "Content-Type": staticContentType(resolvedPath),
10857
- "Cache-Control": "public, max-age=31536000, immutable",
10858
- ...corsHeaders
10859
- };
10860
- return req.method === "HEAD" ? new Response(null, { headers }) : new Response(file, { headers });
10861
- }
10862
- }
10863
- } catch {}
10864
- return new Response("Image not found", { status: 404 });
10865
- }
10866
- if (path === "/_stx/sw.js" && stxConfig.offline?.enabled) {
10867
- const stx = await stxModule;
10868
- return new Response(stx.generateOfflineWorker(stxConfig.offline, getBuildId()), {
10869
- headers: {
10870
- "Content-Type": "text/javascript; charset=utf-8",
10871
- "Cache-Control": "no-cache",
10872
- "Service-Worker-Allowed": "/"
10873
- }
10874
- });
10875
- }
10876
- if (path === "/_stx/stores.js") {
10877
- const stx = await stxModule;
10878
- const code = typeof stx.getStoreScript === "function" ? await stx.getStoreScript() : null;
10879
- return new Response(code ?? "", {
10880
- status: code === null ? 404 : 200,
10881
- headers: {
10882
- "Content-Type": "application/javascript; charset=utf-8",
10883
- "Cache-Control": "no-store",
10884
- ...corsHeaders
10885
- }
10886
- });
10887
- }
10888
- const cssAsset = path.match(/^\/_stx\/css\.([a-f0-9]{16})\.css$/);
10889
- if (cssAsset) {
10890
- const stx = await stxModule;
10891
- const content = stx.getCssServeAsset(cssAsset[1]);
10892
- if (content === undefined)
10893
- return new Response("Css asset not found", { status: 404 });
10894
- const headers = {
10895
- "Content-Type": "text/css; charset=utf-8",
10896
- "Cache-Control": "public, max-age=31536000, immutable",
10897
- ...corsHeaders
10898
- };
10899
- if (req.method === "HEAD")
10900
- return new Response(null, { headers });
10901
- return new Response(content, { headers });
10902
- }
10903
- if (options.onRequest) {
10904
- const hookResult = await options.onRequest(req, server);
10905
- if (hookResult instanceof Response)
10906
- return hookResult;
10907
- if (hookResult && typeof hookResult === "object")
10908
- Object.assign(reqCtx, hookResult);
10909
10981
  }
10910
- const route = resolveRouteMiddleware(path);
10911
- const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
10912
- const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
10913
- if (requested.length > 0) {
10914
- const expanded = expandMiddlewareNames(requested);
10915
- const cookies = parseCookies(req);
10916
- const ctx = {
10917
- path,
10918
- url,
10919
- params: route.params,
10920
- cookies,
10921
- redirect: (to, status = 302) => {
10922
- const sep = to.includes("?") ? "&" : "?";
10923
- const next = encodeURIComponent(path + (url.search || ""));
10924
- return new Response(null, {
10925
- status,
10926
- headers: { Location: `${to}${sep}next=${next}` }
10927
- });
10928
- }
10929
- };
10930
- const middlewareResponse = await runPageMiddleware({
10931
- request: req,
10932
- context: ctx,
10933
- entries: expanded,
10934
- registry: middlewareRegistry,
10935
- prepareRequest: options.prepareMiddlewareRequest
10936
- });
10937
- if (middlewareResponse)
10938
- return middlewareResponse;
10939
- }
10940
- if (options.routes) {
10941
- const routeHandler = options.routes[path];
10942
- if (routeHandler) {
10943
- return routeHandler(req);
10982
+ }
10983
+ if (path.startsWith("/api/data/") && req.method === "POST") {
10984
+ try {
10985
+ const tableName = path.replace("/api/data/", "").split("/")[0];
10986
+ if (!tableName) {
10987
+ return new Response(JSON.stringify({ error: "Table name required" }), {
10988
+ status: 400,
10989
+ headers: { "Content-Type": "application/json", ...corsHeaders }
10990
+ });
10944
10991
  }
10945
- }
10946
- if (path.startsWith("/_stx/component/")) {
10947
- const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
10948
- if (componentName) {
10949
- try {
10950
- const { processDirectives, defaultConfig } = await stxModule;
10951
- const componentTemplate = `<${componentName} />`;
10952
- const componentOpts = {
10953
- ...defaultConfig,
10954
- ...componentsDir && { componentsDir },
10955
- ...layoutsDir && { layoutsDir },
10956
- ...fallbackLayoutsDir && { fallbackLayoutsDir },
10957
- ...fallbackComponentsDir && { fallbackComponentsDir },
10958
- ...partialsDir && { partialsDir },
10959
- autoShell: false
10960
- };
10961
- const html = await processDirectives(componentTemplate, {}, `${componentsDir}/${componentName}.stx`, componentOpts, new Set);
10962
- return new Response(html, {
10963
- headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
10964
- });
10965
- } catch (e) {
10966
- return new Response(`<div class="stx-async-error">${e.message}</div>`, {
10967
- status: 500,
10968
- headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
10969
- });
10970
- }
10992
+ if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
10993
+ return new Response(JSON.stringify({ error: "Invalid table name" }), {
10994
+ status: 400,
10995
+ headers: { "Content-Type": "application/json", ...corsHeaders }
10996
+ });
10971
10997
  }
10972
- }
10973
- if (path.startsWith("/api/data/") && req.method === "POST") {
10998
+ const body = await req.json();
10999
+ const { Database } = await import("bun:sqlite");
11000
+ const dbPath = nodePath.resolve(process20.cwd(), "database/stacks.sqlite");
11001
+ const db = new Database(dbPath);
10974
11002
  try {
10975
- const tableName = path.replace("/api/data/", "").split("/")[0];
10976
- if (!tableName) {
10977
- return new Response(JSON.stringify({ error: "Table name required" }), {
10978
- status: 400,
10979
- headers: { "Content-Type": "application/json", ...corsHeaders }
10980
- });
11003
+ const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
11004
+ const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
11005
+ const columns = [];
11006
+ const placeholders = [];
11007
+ const values = [];
11008
+ for (const col of validColumns) {
11009
+ if (body[col] !== undefined && body[col] !== "") {
11010
+ columns.push(col);
11011
+ placeholders.push("?");
11012
+ values.push(body[col]);
11013
+ }
11014
+ }
11015
+ const now = new Date().toISOString();
11016
+ if (tableInfo.some((c) => c.name === "created_at")) {
11017
+ columns.push("created_at");
11018
+ placeholders.push("?");
11019
+ values.push(now);
10981
11020
  }
10982
- if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
10983
- return new Response(JSON.stringify({ error: "Invalid table name" }), {
11021
+ if (tableInfo.some((c) => c.name === "updated_at")) {
11022
+ columns.push("updated_at");
11023
+ placeholders.push("?");
11024
+ values.push(now);
11025
+ }
11026
+ if (columns.length === 0) {
11027
+ return new Response(JSON.stringify({ error: "No valid fields provided" }), {
10984
11028
  status: 400,
10985
11029
  headers: { "Content-Type": "application/json", ...corsHeaders }
10986
11030
  });
10987
11031
  }
10988
- const body = await req.json();
10989
- const { Database } = await import("bun:sqlite");
10990
- const dbPath = nodePath.resolve(process20.cwd(), "database/stacks.sqlite");
10991
- const db = new Database(dbPath);
10992
- try {
10993
- const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
10994
- const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
10995
- const columns = [];
10996
- const placeholders = [];
10997
- const values = [];
10998
- for (const col of validColumns) {
10999
- if (body[col] !== undefined && body[col] !== "") {
11000
- columns.push(col);
11001
- placeholders.push("?");
11002
- values.push(body[col]);
11003
- }
11004
- }
11005
- const now = new Date().toISOString();
11006
- if (tableInfo.some((c) => c.name === "created_at")) {
11007
- columns.push("created_at");
11008
- placeholders.push("?");
11009
- values.push(now);
11010
- }
11011
- if (tableInfo.some((c) => c.name === "updated_at")) {
11012
- columns.push("updated_at");
11013
- placeholders.push("?");
11014
- values.push(now);
11015
- }
11016
- if (columns.length === 0) {
11017
- return new Response(JSON.stringify({ error: "No valid fields provided" }), {
11018
- status: 400,
11019
- headers: { "Content-Type": "application/json", ...corsHeaders }
11020
- });
11032
+ const query = `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
11033
+ const stmt = db.prepare(query);
11034
+ const result = stmt.run(...values);
11035
+ return new Response(JSON.stringify({
11036
+ success: true,
11037
+ id: result.lastInsertRowid,
11038
+ message: "Record created successfully"
11039
+ }), {
11040
+ status: 201,
11041
+ headers: { "Content-Type": "application/json", ...corsHeaders }
11042
+ });
11043
+ } catch (error) {
11044
+ console.error("API Error:", error);
11045
+ return new Response(JSON.stringify({
11046
+ error: error.message || "Failed to create record"
11047
+ }), {
11048
+ status: 500,
11049
+ headers: { "Content-Type": "application/json", ...corsHeaders }
11050
+ });
11051
+ } finally {
11052
+ db.close();
11053
+ }
11054
+ } catch {}
11055
+ }
11056
+ if (path === "/index")
11057
+ path = "/";
11058
+ if (path === "/") {
11059
+ const indexContent = await getRoute("/", reqCtx);
11060
+ if (!indexContent) {
11061
+ const homeContent = await getRoute("/home", reqCtx);
11062
+ if (homeContent) {
11063
+ return new Response(null, {
11064
+ status: 302,
11065
+ headers: {
11066
+ Location: "/home",
11067
+ ...corsHeaders
11021
11068
  }
11022
- const query = `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
11023
- const stmt = db.prepare(query);
11024
- const result = stmt.run(...values);
11025
- return new Response(JSON.stringify({
11026
- success: true,
11027
- id: result.lastInsertRowid,
11028
- message: "Record created successfully"
11029
- }), {
11030
- status: 201,
11031
- headers: { "Content-Type": "application/json", ...corsHeaders }
11032
- });
11033
- } catch (error) {
11034
- console.error("API Error:", error);
11035
- return new Response(JSON.stringify({
11036
- error: error.message || "Failed to create record"
11037
- }), {
11038
- status: 500,
11039
- headers: { "Content-Type": "application/json", ...corsHeaders }
11040
- });
11041
- } finally {
11042
- db.close();
11043
- }
11044
- } catch {}
11045
- }
11046
- if (path === "/index")
11047
- path = "/";
11048
- if (path === "/") {
11049
- const indexContent = await getRoute("/", reqCtx);
11050
- if (!indexContent) {
11051
- const homeContent = await getRoute("/home", reqCtx);
11052
- if (homeContent) {
11053
- return new Response(null, {
11054
- status: 302,
11055
- headers: {
11056
- Location: "/home",
11057
- ...corsHeaders
11058
- }
11059
- });
11060
- }
11069
+ });
11061
11070
  }
11062
11071
  }
11063
- const content = await getRoute(path, reqCtx);
11064
- if (reqCtx.actionRedirect) {
11065
- const redirectResponse = actionRedirectResponse(reqCtx.actionRedirect, reqCtx.actionCookies, reqCtx.responseHeaders);
11066
- for (const [key, value] of Object.entries(corsHeaders))
11067
- redirectResponse.headers.set(key, value);
11068
- return redirectResponse;
11069
- }
11070
- if (content) {
11071
- const responseStatus = reqCtx.responseStatus ?? 200;
11072
- const isSpaNav = req.headers.get(SPA_NAV_HEADER) === "true";
11073
- if (isSpaNav) {
11074
- const layoutMetadata = extractLayoutMetadata(content);
11075
- const pageLayout = layoutMetadata.layout;
11076
- const pageLayoutGroup = i18nConfig && i18nLocale ? `i18n:${i18nLocale}` : layoutMetadata.group;
11077
- let fragment = content;
11078
- let containerAttrs = "";
11079
- let mainContentStart = -1;
11080
- let mainContentEnd = -1;
11081
- const headStyles = [];
11082
- const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
11083
- const withoutScripts = (html) => html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
11084
- if (headMatch) {
11085
- const headContent = withoutScripts(headMatch[1]);
11086
- let styleMatch;
11087
- const styleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
11088
- while ((styleMatch = styleRe.exec(headContent)) !== null) {
11089
- if (styleMatch[0].includes("data-css")) {
11090
- const cssContent = styleMatch[0].replace(/<style[^>]*>/, "").replace(/<\/style>/, "");
11091
- const hiddenRule = cssContent.indexOf("[hidden]");
11092
- if (hiddenRule !== -1) {
11093
- const afterPreflight = cssContent.indexOf("}", hiddenRule) + 1;
11094
- const utilities = cssContent.slice(afterPreflight).trim();
11095
- if (utilities) {
11096
- headStyles.push(`<style data-css="fragment">${utilities}</style>`);
11097
- }
11072
+ }
11073
+ const content = await getRoute(path, reqCtx);
11074
+ if (reqCtx.actionRedirect) {
11075
+ const redirectResponse = actionRedirectResponse(reqCtx.actionRedirect, reqCtx.actionCookies, reqCtx.responseHeaders);
11076
+ for (const [key, value] of Object.entries(corsHeaders))
11077
+ redirectResponse.headers.set(key, value);
11078
+ return redirectResponse;
11079
+ }
11080
+ if (content) {
11081
+ const responseStatus = reqCtx.responseStatus ?? 200;
11082
+ const isSpaNav = req.headers.get(SPA_NAV_HEADER) === "true";
11083
+ if (isSpaNav) {
11084
+ const layoutMetadata = extractLayoutMetadata(content);
11085
+ const pageLayout = layoutMetadata.layout;
11086
+ const pageLayoutGroup = i18nConfig && i18nLocale ? `i18n:${i18nLocale}` : layoutMetadata.group;
11087
+ let fragment = content;
11088
+ let containerAttrs = "";
11089
+ let mainContentStart = -1;
11090
+ let mainContentEnd = -1;
11091
+ const headStyles = [];
11092
+ const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
11093
+ const withoutScripts = (html) => html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
11094
+ if (headMatch) {
11095
+ const headContent = withoutScripts(headMatch[1]);
11096
+ let styleMatch;
11097
+ const styleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
11098
+ while ((styleMatch = styleRe.exec(headContent)) !== null) {
11099
+ if (styleMatch[0].includes("data-css")) {
11100
+ const cssContent = styleMatch[0].replace(/<style[^>]*>/, "").replace(/<\/style>/, "");
11101
+ const hiddenRule = cssContent.indexOf("[hidden]");
11102
+ if (hiddenRule !== -1) {
11103
+ const afterPreflight = cssContent.indexOf("}", hiddenRule) + 1;
11104
+ const utilities = cssContent.slice(afterPreflight).trim();
11105
+ if (utilities) {
11106
+ headStyles.push(`<style data-css="fragment">${utilities}</style>`);
11098
11107
  }
11099
- continue;
11100
11108
  }
11101
- headStyles.push(styleMatch[0]);
11102
- }
11103
- const cssLinkRe = /<link\b[^>]*\bdata-css=(?:"generated"|'generated')[^>]*>/gi;
11104
- let cssLinkMatch;
11105
- while ((cssLinkMatch = cssLinkRe.exec(headContent)) !== null) {
11106
- headStyles.push(cssLinkMatch[0]);
11109
+ continue;
11107
11110
  }
11111
+ headStyles.push(styleMatch[0]);
11108
11112
  }
11109
- const containerSelector = stxConfig?.router?.container || "main";
11110
- const containerRegion = findContainerRegion(fragment, containerSelector);
11111
- const containerPath = new URL(req.url).pathname;
11112
- if (!containerRegion && !warnedMissingContainer.has(containerPath)) {
11113
- warnedMissingContainer.add(containerPath);
11114
- console.warn(`[stx] No SPA swap container matching "${containerSelector}" in ${containerPath}. SPA navigation is disabled for links on this page; the full document will load instead. Add a matching element, or point router.container at one this page has.`);
11113
+ const cssLinkRe = /<link\b[^>]*\bdata-css=(?:"generated"|'generated')[^>]*>/gi;
11114
+ let cssLinkMatch;
11115
+ while ((cssLinkMatch = cssLinkRe.exec(headContent)) !== null) {
11116
+ headStyles.push(cssLinkMatch[0]);
11115
11117
  }
11116
- const mainOpenMatch = containerRegion ? { 0: containerRegion.openTag, index: containerRegion.openIndex } : null;
11117
- const mainCloseIdx = containerRegion ? containerRegion.end : -1;
11118
- if (mainOpenMatch && mainCloseIdx !== -1) {
11119
- const bodyMatch = content.match(/<body\b[^>]*>/i);
11120
- if (bodyMatch) {
11121
- const bodyStart = bodyMatch.index + bodyMatch[0].length;
11122
- const mainIdx = mainOpenMatch.index;
11123
- const beforeMain = withoutScripts(content.slice(bodyStart, mainIdx));
11124
- let bodyStyleMatch;
11125
- const bodyStyleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
11126
- while ((bodyStyleMatch = bodyStyleRe.exec(beforeMain)) !== null) {
11127
- headStyles.push(bodyStyleMatch[0]);
11128
- }
11118
+ }
11119
+ const containerSelector = stxConfig?.router?.container || "main";
11120
+ const containerRegion = findContainerRegion(fragment, containerSelector);
11121
+ const containerPath = new URL(req.url).pathname;
11122
+ if (!containerRegion && !warnedMissingContainer.has(containerPath)) {
11123
+ warnedMissingContainer.add(containerPath);
11124
+ console.warn(`[stx] No SPA swap container matching "${containerSelector}" in ${containerPath}. SPA navigation is disabled for links on this page; the full document will load instead. Add a matching element, or point router.container at one this page has.`);
11125
+ }
11126
+ const mainOpenMatch = containerRegion ? { 0: containerRegion.openTag, index: containerRegion.openIndex } : null;
11127
+ const mainCloseIdx = containerRegion ? containerRegion.end : -1;
11128
+ if (mainOpenMatch && mainCloseIdx !== -1) {
11129
+ const bodyMatch = content.match(/<body\b[^>]*>/i);
11130
+ if (bodyMatch) {
11131
+ const bodyStart = bodyMatch.index + bodyMatch[0].length;
11132
+ const mainIdx = mainOpenMatch.index;
11133
+ const beforeMain = withoutScripts(content.slice(bodyStart, mainIdx));
11134
+ let bodyStyleMatch;
11135
+ const bodyStyleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
11136
+ while ((bodyStyleMatch = bodyStyleRe.exec(beforeMain)) !== null) {
11137
+ headStyles.push(bodyStyleMatch[0]);
11129
11138
  }
11130
- containerAttrs = mainOpenMatch[0].replace(new RegExp(`^<${containerRegion.tagName}\\b`, "i"), "").replace(/\/?>$/, "").trim();
11131
- const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
11132
- mainContentStart = mainStart;
11133
- mainContentEnd = mainCloseIdx;
11134
- fragment = fragment.slice(mainStart, mainCloseIdx).trim();
11135
- }
11136
- const externalPageScripts = containerRegion ? externalScriptsOutsideContainer(content, containerRegion) : [];
11137
- const pageSetupScripts = [];
11138
- const pageSetupScriptOffsets = new Set;
11139
- const appendOutsideMainScript = (match) => {
11140
- const offset = match.index;
11141
- const insideMain = mainContentStart !== -1 && offset >= mainContentStart && offset < mainContentEnd;
11142
- if (insideMain || pageSetupScriptOffsets.has(offset))
11143
- return;
11144
- if (mainContentStart !== -1 && isChromeInstanceScript(content, match[0], mainContentStart, mainContentEnd))
11145
- return;
11146
- pageSetupScriptOffsets.add(offset);
11147
- pageSetupScripts.push(match[0]);
11148
- };
11149
- const routeParamsRe = /<script\b[^>]*data-stx-route-params[^>]*>[\s\S]*?<\/script>/gi;
11150
- let setupMatch;
11151
- while ((setupMatch = routeParamsRe.exec(content)) !== null) {
11152
- appendOutsideMainScript(setupMatch);
11153
11139
  }
11154
- const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
11155
- while ((setupMatch = allScriptRe.exec(content)) !== null) {
11156
- const scriptContent = setupMatch[0];
11157
- if (scriptContent.includes("__stx_early_mounts"))
11158
- continue;
11159
- if (scriptContent.includes("data-stx-reactive") && scriptContent.includes("window.__stx_reactive"))
11160
- continue;
11161
- appendOutsideMainScript(setupMatch);
11162
- }
11163
- const bridgeInitRe = /<script\b[^>]*data-stx-reactive[^>]*>(?![\s\S]*window\.__stx_reactive)[\s\S]*?<\/script>/gi;
11164
- while ((setupMatch = bridgeInitRe.exec(content)) !== null) {
11165
- appendOutsideMainScript(setupMatch);
11166
- }
11167
- fragment = fragment.replace(/<script data-stx-scoped>\s*;?\(function\(\)\s*\{[\s\S]*?<\/script>/g, "");
11168
- const clearStale = "<script data-stx-page>if(window.stx)window.stx._latestSetup=null;</script>";
11169
- const inlinePageScripts = pageSetupScripts.filter((script) => !externalPageScripts.includes(script));
11170
- const componentFactoryScripts = inlinePageScripts.filter((script) => script.includes("data-stx-component-factories"));
11171
- const trailingPageScripts = inlinePageScripts.filter((script) => !script.includes("data-stx-component-factories"));
11172
- fragment = `${headStyles.join(`
11140
+ containerAttrs = mainOpenMatch[0].replace(new RegExp(`^<${containerRegion.tagName}\\b`, "i"), "").replace(/\/?>$/, "").trim();
11141
+ const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
11142
+ mainContentStart = mainStart;
11143
+ mainContentEnd = mainCloseIdx;
11144
+ fragment = fragment.slice(mainStart, mainCloseIdx).trim();
11145
+ }
11146
+ const externalPageScripts = containerRegion ? externalScriptsOutsideContainer(content, containerRegion) : [];
11147
+ const pageSetupScripts = [];
11148
+ const pageSetupScriptOffsets = new Set;
11149
+ const appendOutsideMainScript = (match) => {
11150
+ const offset = match.index;
11151
+ const insideMain = mainContentStart !== -1 && offset >= mainContentStart && offset < mainContentEnd;
11152
+ if (insideMain || pageSetupScriptOffsets.has(offset))
11153
+ return;
11154
+ if (mainContentStart !== -1 && isChromeInstanceScript(content, match[0], mainContentStart, mainContentEnd))
11155
+ return;
11156
+ pageSetupScriptOffsets.add(offset);
11157
+ pageSetupScripts.push(match[0]);
11158
+ };
11159
+ const routeParamsRe = /<script\b[^>]*data-stx-route-params[^>]*>[\s\S]*?<\/script>/gi;
11160
+ let setupMatch;
11161
+ while ((setupMatch = routeParamsRe.exec(content)) !== null) {
11162
+ appendOutsideMainScript(setupMatch);
11163
+ }
11164
+ const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
11165
+ while ((setupMatch = allScriptRe.exec(content)) !== null) {
11166
+ const scriptContent = setupMatch[0];
11167
+ if (scriptContent.includes("__stx_early_mounts"))
11168
+ continue;
11169
+ if (scriptContent.includes("data-stx-reactive") && scriptContent.includes("window.__stx_reactive"))
11170
+ continue;
11171
+ appendOutsideMainScript(setupMatch);
11172
+ }
11173
+ const bridgeInitRe = /<script\b[^>]*data-stx-reactive[^>]*>(?![\s\S]*window\.__stx_reactive)[\s\S]*?<\/script>/gi;
11174
+ while ((setupMatch = bridgeInitRe.exec(content)) !== null) {
11175
+ appendOutsideMainScript(setupMatch);
11176
+ }
11177
+ fragment = fragment.replace(/<script data-stx-scoped>\s*;?\(function\(\)\s*\{[\s\S]*?<\/script>/g, "");
11178
+ const clearStale = "<script data-stx-page>if(window.stx)window.stx._latestSetup=null;</script>";
11179
+ const inlinePageScripts = pageSetupScripts.filter((script) => !externalPageScripts.includes(script));
11180
+ const componentFactoryScripts = inlinePageScripts.filter((script) => script.includes("data-stx-component-factories"));
11181
+ const trailingPageScripts = inlinePageScripts.filter((script) => !script.includes("data-stx-component-factories"));
11182
+ fragment = `${headStyles.join(`
11173
11183
  `)}
11174
11184
  ${externalPageScripts.join(`
11175
11185
  `)}
@@ -11179,232 +11189,285 @@ ${fragment}
11179
11189
  ${clearStale}
11180
11190
  ${trailingPageScripts.join(`
11181
11191
  `)}`;
11182
- const titleMatch = content.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
11183
- const pageTitle = titleMatch ? decodeTitleEntities(titleMatch[1].trim()) : "";
11184
- const shipsRuntime = content.includes("data-stx-runtime");
11185
- return new Response(fragment, {
11186
- status: isRenderFailure(content) ? 500 : responseStatus,
11187
- headers: {
11188
- "Content-Type": "text/html; charset=utf-8",
11189
- "X-STX-Fragment": "true",
11190
- "X-STX-Layout": pageLayout,
11191
- "X-STX-Layout-Group": pageLayoutGroup,
11192
- "X-STX-Runtime": shipsRuntime ? "true" : "false",
11193
- ...pageTitle && { "X-STX-Title": encodeURIComponent(pageTitle) },
11194
- [BUILD_ID_HEADER]: getBuildId(),
11195
- ...containerAttrs && { "X-STX-Container-Attrs": encodeURIComponent(containerAttrs) },
11196
- ...spaNavVaryHeaders(),
11197
- "Cache-Control": FRAGMENT_CACHE_CONTROL,
11198
- ...corsHeaders
11192
+ const titleMatch = content.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
11193
+ const pageTitle = titleMatch ? decodeTitleEntities(titleMatch[1].trim()) : "";
11194
+ const shipsRuntime = content.includes("data-stx-runtime");
11195
+ return new Response(fragment, {
11196
+ status: isRenderFailure(content) ? 500 : responseStatus,
11197
+ headers: {
11198
+ "Content-Type": "text/html; charset=utf-8",
11199
+ "X-STX-Fragment": "true",
11200
+ "X-STX-Layout": pageLayout,
11201
+ "X-STX-Layout-Group": pageLayoutGroup,
11202
+ "X-STX-Runtime": shipsRuntime ? "true" : "false",
11203
+ ...pageTitle && { "X-STX-Title": encodeURIComponent(pageTitle) },
11204
+ [BUILD_ID_HEADER]: getBuildId(),
11205
+ ...containerAttrs && { "X-STX-Container-Attrs": encodeURIComponent(containerAttrs) },
11206
+ ...spaNavVaryHeaders(),
11207
+ "Cache-Control": FRAGMENT_CACHE_CONTROL,
11208
+ ...corsHeaders
11209
+ }
11210
+ });
11211
+ }
11212
+ let cleaned = content;
11213
+ let runtimeCount = 0;
11214
+ cleaned = content.replace(/<script data-stx-scoped>\(function\(\)\{'use strict';var cloakStyle[\s\S]*?<\/script>/g, (match) => {
11215
+ runtimeCount++;
11216
+ return runtimeCount === 1 ? match : "";
11217
+ });
11218
+ const pageRenderFailed = isRenderFailure(cleaned);
11219
+ if (pageRenderFailed) {
11220
+ console.error(`[stx] render failed for ${new URL(req.url).pathname} \u2014 serving 500`);
11221
+ for (const custom500 of ["/500", "/errors/500"]) {
11222
+ try {
11223
+ const errorPage = usableErrorPage(await getRoute(custom500, reqCtx));
11224
+ if (errorPage) {
11225
+ const isProd = isProductionServe();
11226
+ return new Response(isProd ? errorPage : injectHmrClient(errorPage), {
11227
+ status: 500,
11228
+ headers: {
11229
+ "Content-Type": "text/html; charset=utf-8",
11230
+ "Cache-Control": "no-store",
11231
+ ...corsHeaders
11232
+ }
11233
+ });
11199
11234
  }
11200
- });
11235
+ } catch {}
11201
11236
  }
11202
- let cleaned = content;
11203
- let runtimeCount = 0;
11204
- cleaned = content.replace(/<script data-stx-scoped>\(function\(\)\{'use strict';var cloakStyle[\s\S]*?<\/script>/g, (match) => {
11205
- runtimeCount++;
11206
- return runtimeCount === 1 ? match : "";
11207
- });
11208
- const pageRenderFailed = isRenderFailure(cleaned);
11209
- if (pageRenderFailed) {
11210
- console.error(`[stx] render failed for ${new URL(req.url).pathname} \u2014 serving 500`);
11211
- for (const custom500 of ["/500", "/errors/500"]) {
11212
- try {
11213
- const errorPage = usableErrorPage(await getRoute(custom500, reqCtx));
11214
- if (errorPage) {
11215
- const isProd = isProductionServe();
11216
- return new Response(isProd ? errorPage : injectHmrClient(errorPage), {
11217
- status: 500,
11218
- headers: {
11219
- "Content-Type": "text/html; charset=utf-8",
11220
- "Cache-Control": "no-store",
11221
- ...corsHeaders
11222
- }
11223
- });
11224
- }
11225
- } catch {}
11237
+ }
11238
+ const pageHeaders = new Headers({
11239
+ "Content-Type": "text/html; charset=utf-8",
11240
+ ...spaNavVaryHeaders(),
11241
+ "Cache-Control": "no-store",
11242
+ ...corsHeaders
11243
+ });
11244
+ for (const [name, value] of Object.entries(reqCtx.responseHeaders ?? {}))
11245
+ pageHeaders.set(name, value);
11246
+ for (const cookie of reqCtx.actionCookies ?? [])
11247
+ pageHeaders.append("Set-Cookie", cookie);
11248
+ return new Response(injectHmrClient(cleaned), {
11249
+ status: pageRenderFailed ? 500 : responseStatus,
11250
+ headers: pageHeaders
11251
+ });
11252
+ }
11253
+ if (!isStaticAssetPath(path)) {
11254
+ const contentWithExt = await getRoute(`${path}.html`, reqCtx);
11255
+ if (contentWithExt) {
11256
+ return new Response(injectHmrClient(contentWithExt), {
11257
+ status: reqCtx.responseStatus ?? 200,
11258
+ headers: {
11259
+ "Content-Type": "text/html; charset=utf-8",
11260
+ "Cache-Control": "no-store",
11261
+ ...corsHeaders
11226
11262
  }
11227
- }
11228
- const pageHeaders = new Headers({
11229
- "Content-Type": "text/html; charset=utf-8",
11230
- ...spaNavVaryHeaders(),
11231
- "Cache-Control": "no-store",
11232
- ...corsHeaders
11233
- });
11234
- for (const [name, value] of Object.entries(reqCtx.responseHeaders ?? {}))
11235
- pageHeaders.set(name, value);
11236
- for (const cookie of reqCtx.actionCookies ?? [])
11237
- pageHeaders.append("Set-Cookie", cookie);
11238
- return new Response(injectHmrClient(cleaned), {
11239
- status: pageRenderFailed ? 500 : responseStatus,
11240
- headers: pageHeaders
11241
11263
  });
11242
11264
  }
11243
- if (!isStaticAssetPath(path)) {
11244
- const contentWithExt = await getRoute(`${path}.html`, reqCtx);
11245
- if (contentWithExt) {
11246
- return new Response(injectHmrClient(contentWithExt), {
11247
- status: reqCtx.responseStatus ?? 200,
11265
+ }
11266
+ if (path.startsWith("/chunk-") || path.endsWith(".js") || path.endsWith(".css")) {
11267
+ try {
11268
+ const buildFile = Bun.file(stateDir(process20.cwd(), path));
11269
+ if (await buildFile.exists()) {
11270
+ const ext = path.split(".").pop()?.toLowerCase();
11271
+ const contentType = ext === "css" ? "text/css" : ext === "js" ? "application/javascript" : "text/plain";
11272
+ return new Response(buildFile, {
11248
11273
  headers: {
11249
- "Content-Type": "text/html; charset=utf-8",
11250
- "Cache-Control": "no-store",
11251
- ...corsHeaders
11274
+ "Content-Type": contentType,
11275
+ "Cache-Control": "no-store"
11252
11276
  }
11253
11277
  });
11254
11278
  }
11279
+ } catch {}
11280
+ }
11281
+ if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
11282
+ await ensureAssets();
11283
+ let assetPathname;
11284
+ try {
11285
+ assetPathname = decodeURIComponent(path);
11286
+ } catch {
11287
+ return new Response("Invalid asset path", { status: 400 });
11255
11288
  }
11256
- if (path.startsWith("/chunk-") || path.endsWith(".js") || path.endsWith(".css")) {
11289
+ if (!isSafeAssetPath(assetPathname))
11290
+ return new Response("Invalid asset path", { status: 400 });
11291
+ for (const assetPath of assetRequestPaths(assetPathname)) {
11257
11292
  try {
11258
- const buildFile = Bun.file(stateDir(process20.cwd(), path));
11259
- if (await buildFile.exists()) {
11260
- const ext = path.split(".").pop()?.toLowerCase();
11261
- const contentType = ext === "css" ? "text/css" : ext === "js" ? "application/javascript" : "text/plain";
11262
- return new Response(buildFile, {
11293
+ const filePath = nodePath.resolve(process20.cwd(), `.${assetPath}`);
11294
+ const file = Bun.file(filePath);
11295
+ if (await file.exists()) {
11296
+ const ext = getAssetExtension(assetPath);
11297
+ if (isBundledAssetExtension(ext))
11298
+ return bundleBrowserAsset(filePath);
11299
+ return new Response(file, {
11263
11300
  headers: {
11264
- "Content-Type": contentType,
11301
+ "Content-Type": staticContentType(assetPath),
11265
11302
  "Cache-Control": "no-store"
11266
11303
  }
11267
11304
  });
11268
11305
  }
11269
- } catch {}
11270
- }
11271
- if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
11272
- await ensureAssets();
11273
- let assetPathname;
11274
- try {
11275
- assetPathname = decodeURIComponent(path);
11276
11306
  } catch {
11277
- return new Response("Invalid asset path", { status: 400 });
11307
+ continue;
11278
11308
  }
11279
- if (!isSafeAssetPath(assetPathname))
11280
- return new Response("Invalid asset path", { status: 400 });
11281
- for (const assetPath of assetRequestPaths(assetPathname)) {
11309
+ }
11310
+ }
11311
+ if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
11312
+ const publicRoot = nodePath.resolve(process20.cwd(), publicDir);
11313
+ let safePathname;
11314
+ try {
11315
+ safePathname = decodeURIComponent(path);
11316
+ } catch {
11317
+ safePathname = path;
11318
+ }
11319
+ if (!safePathname.includes("\x00")) {
11320
+ const resolvedPath = nodePath.resolve(publicRoot, `.${safePathname}`);
11321
+ const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath.sep}`);
11322
+ if (isInsidePublicRoot) {
11282
11323
  try {
11283
- const filePath = nodePath.resolve(process20.cwd(), `.${assetPath}`);
11284
- const file = Bun.file(filePath);
11324
+ const file = Bun.file(resolvedPath);
11285
11325
  if (await file.exists()) {
11286
- const ext = getAssetExtension(assetPath);
11287
- if (isBundledAssetExtension(ext))
11288
- return bundleBrowserAsset(filePath);
11289
- return new Response(file, {
11290
- headers: {
11291
- "Content-Type": staticContentType(assetPath),
11292
- "Cache-Control": "no-store"
11293
- }
11294
- });
11326
+ const stat = await file.stat().catch(() => null);
11327
+ if (stat && !stat.isDirectory()) {
11328
+ return new Response(file, {
11329
+ headers: {
11330
+ "Content-Type": staticContentType(resolvedPath),
11331
+ "Cache-Control": staticCacheControl(resolvedPath)
11332
+ }
11333
+ });
11334
+ }
11295
11335
  }
11296
- } catch {
11297
- continue;
11298
- }
11336
+ } catch {}
11299
11337
  }
11300
11338
  }
11301
- if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
11302
- const publicRoot = nodePath.resolve(process20.cwd(), publicDir);
11303
- let safePathname;
11304
- try {
11305
- safePathname = decodeURIComponent(path);
11306
- } catch {
11307
- safePathname = path;
11339
+ }
11340
+ const isProd = isProductionServe();
11341
+ if (path === "/favicon.ico" && !isProd) {
11342
+ return new Response(null, { status: 204 });
11343
+ }
11344
+ if (isFileRequestPath(path)) {
11345
+ return new Response("Not Found", {
11346
+ status: 404,
11347
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }
11348
+ });
11349
+ }
11350
+ for (const custom404 of ["/404", "/errors/404"]) {
11351
+ try {
11352
+ const customContent = await getRoute(custom404, reqCtx);
11353
+ if (customContent) {
11354
+ return new Response(isProd ? customContent : injectHmrClient(customContent), {
11355
+ status: 404,
11356
+ headers: { "Content-Type": "text/html; charset=utf-8" }
11357
+ });
11308
11358
  }
11309
- if (!safePathname.includes("\x00")) {
11310
- const resolvedPath = nodePath.resolve(publicRoot, `.${safePathname}`);
11311
- const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath.sep}`);
11312
- if (isInsidePublicRoot) {
11313
- try {
11314
- const file = Bun.file(resolvedPath);
11315
- if (await file.exists()) {
11316
- const stat = await file.stat().catch(() => null);
11317
- if (stat && !stat.isDirectory()) {
11318
- return new Response(file, {
11319
- headers: {
11320
- "Content-Type": staticContentType(resolvedPath),
11321
- "Cache-Control": staticCacheControl(resolvedPath)
11322
- }
11323
- });
11324
- }
11325
- }
11326
- } catch {}
11359
+ } catch {}
11360
+ }
11361
+ let availableRoutes;
11362
+ if (!isProd) {
11363
+ const files = await discoverFiles();
11364
+ availableRoutes = [];
11365
+ for (const filePath of files) {
11366
+ let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
11367
+ for (const pattern of patterns) {
11368
+ const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
11369
+ if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
11370
+ normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
11371
+ break;
11327
11372
  }
11328
11373
  }
11329
- }
11330
- const isProd = isProductionServe();
11331
- if (path === "/favicon.ico" && !isProd) {
11332
- return new Response(null, { status: 204 });
11333
- }
11334
- if (isFileRequestPath(path)) {
11335
- return new Response("Not Found", {
11336
- status: 404,
11337
- headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }
11338
- });
11339
- }
11340
- for (const custom404 of ["/404", "/errors/404"]) {
11341
- try {
11342
- const customContent = await getRoute(custom404, reqCtx);
11343
- if (customContent) {
11344
- return new Response(isProd ? customContent : injectHmrClient(customContent), {
11345
- status: 404,
11346
- headers: { "Content-Type": "text/html; charset=utf-8" }
11347
- });
11348
- }
11349
- } catch {}
11350
- }
11351
- let availableRoutes;
11352
- if (!isProd) {
11353
- const files = await discoverFiles();
11354
- availableRoutes = [];
11355
- for (const filePath of files) {
11356
- let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
11357
- for (const pattern of patterns) {
11358
- const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
11359
- if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
11360
- normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
11361
- break;
11362
- }
11363
- }
11364
- let route = normalizedPath.replace(/\.(stx|md|html)$/, "");
11365
- if (route.startsWith("pages/")) {
11366
- route = route.slice(6);
11367
- }
11368
- availableRoutes.push(route);
11374
+ let route = normalizedPath.replace(/\.(stx|md|html)$/, "");
11375
+ if (route.startsWith("pages/")) {
11376
+ route = route.slice(6);
11369
11377
  }
11378
+ availableRoutes.push(route);
11370
11379
  }
11371
- const notFoundHtml = render404Page({ path, routes: availableRoutes, isProduction: isProd });
11372
- return new Response(isProd ? notFoundHtml : injectHmrClient(notFoundHtml), {
11373
- status: 404,
11374
- headers: { "Content-Type": "text/html; charset=utf-8" }
11375
- });
11376
- } finally {
11377
- activeServeLocale = null;
11378
- activeServeSearch = "";
11379
- activeServeHost = "";
11380
- activeServeCookieHeader = "";
11381
- activeServeCookies = {};
11382
- activeServeIp = "";
11383
- }
11384
- })();
11385
- let _finalResp = await applyI18nToResponse(_i18nResp, i18nLocale ?? (i18nConfig?.defaultLocale ?? "en"), path);
11386
- if (mintedCsrfToken && responseTakesCsrfCookie(_finalResp)) {
11387
- try {
11388
- _finalResp.headers.append("Set-Cookie", csrfCookieHeader(mintedCsrfToken, req.url.startsWith("https://")));
11389
- } catch {
11390
- const headers = new Headers(_finalResp.headers);
11391
- headers.append("Set-Cookie", csrfCookieHeader(mintedCsrfToken, req.url.startsWith("https://")));
11392
- _finalResp = new Response(_finalResp.body, {
11393
- status: _finalResp.status,
11394
- statusText: _finalResp.statusText,
11395
- headers
11396
- });
11397
11380
  }
11381
+ const notFoundHtml = render404Page({ path, routes: availableRoutes, isProduction: isProd });
11382
+ return new Response(isProd ? notFoundHtml : injectHmrClient(notFoundHtml), {
11383
+ status: 404,
11384
+ headers: { "Content-Type": "text/html; charset=utf-8" }
11385
+ });
11386
+ } finally {
11387
+ activeServeLocale = null;
11388
+ activeServeSearch = "";
11389
+ activeServeHost = "";
11390
+ activeServeCookieHeader = "";
11391
+ activeServeCookies = {};
11392
+ activeServeIp = "";
11398
11393
  }
11399
- if (options.onResponse) {
11400
- const hooked = await options.onResponse(req, _finalResp);
11401
- if (hooked instanceof Response)
11402
- return hooked;
11394
+ })();
11395
+ let _finalResp = await applyI18nToResponse(_i18nResp, i18nLocale ?? (i18nConfig?.defaultLocale ?? "en"), path);
11396
+ if (mintedCsrfToken && responseTakesCsrfCookie(_finalResp)) {
11397
+ try {
11398
+ _finalResp.headers.append("Set-Cookie", csrfCookieHeader(mintedCsrfToken, req.url.startsWith("https://")));
11399
+ } catch {
11400
+ const headers = new Headers(_finalResp.headers);
11401
+ headers.append("Set-Cookie", csrfCookieHeader(mintedCsrfToken, req.url.startsWith("https://")));
11402
+ _finalResp = new Response(_finalResp.body, {
11403
+ status: _finalResp.status,
11404
+ statusText: _finalResp.statusText,
11405
+ headers
11406
+ });
11403
11407
  }
11404
- return _finalResp;
11405
- })());
11408
+ }
11409
+ if (options.onResponse) {
11410
+ const hooked = await options.onResponse(req, _finalResp);
11411
+ if (hooked instanceof Response)
11412
+ return hooked;
11413
+ }
11414
+ return _finalResp;
11415
+ })());
11416
+ }
11417
+ });
11418
+ }
11419
+ if (production && pageWarmup.enabled) {
11420
+ const warmupStartedAt = performance.now();
11421
+ const warming = warmPages();
11422
+ const done = await settleWithin(warming, pageWarmup.budgetMs);
11423
+ if (!done) {
11424
+ console.warn(`[stx] page warm-up still running after ${formatSeconds(pageWarmup.budgetMs)}; binding now and finishing it in the background.`);
11425
+ }
11426
+ warming.then(({ pages, failed }) => {
11427
+ if (!options.quiet)
11428
+ console.log(`[stx] warmed ${pages} pages in ${formatSeconds(performance.now() - warmupStartedAt)}${failed ? ` (${failed} answered 5xx or failed)` : ""}`);
11429
+ });
11430
+ }
11431
+ async function warmPages() {
11432
+ const files = await discoverFiles();
11433
+ const routes = [...new Set([
11434
+ ...files.map(stxViewFileToRoutePath).filter((routePath) => routePath !== null),
11435
+ ...discoveredPagePaths ?? [],
11436
+ ...pageWarmup.paths
11437
+ ])];
11438
+ const warm = listen(0, false, "127.0.0.1");
11439
+ let next = 0;
11440
+ let failed = 0;
11441
+ try {
11442
+ await Promise.all(Array.from({ length: Math.min(pageWarmup.concurrency, routes.length) }, async () => {
11443
+ while (next < routes.length) {
11444
+ const routePath = routes[next++];
11445
+ try {
11446
+ const response = await fetch(`http://127.0.0.1:${warm.port}${routePath}`, {
11447
+ headers: { [PAGE_WARMUP_HEADER]: "1", Accept: "text/html" },
11448
+ redirect: "manual"
11449
+ });
11450
+ await response.arrayBuffer();
11451
+ if (response.status >= 500)
11452
+ failed++;
11453
+ } catch {
11454
+ failed++;
11455
+ }
11406
11456
  }
11407
- });
11457
+ }));
11458
+ } finally {
11459
+ warm.stop(true);
11460
+ }
11461
+ return { pages: routes.length, failed };
11462
+ }
11463
+ const portAutoIncrement = options.autoIncrementPort ?? true;
11464
+ const maxPortAttempts = portAutoIncrement === false ? 1 : typeof portAutoIncrement === "number" ? Math.max(1, portAutoIncrement) : 10;
11465
+ let actualPort = port;
11466
+ let _server;
11467
+ let lastServeError;
11468
+ for (let attempt = 0;attempt < maxPortAttempts; attempt++) {
11469
+ try {
11470
+ _server = listen(actualPort, options.reusePort ?? false);
11408
11471
  break;
11409
11472
  } catch (err) {
11410
11473
  lastServeError = err;
@@ -11575,7 +11638,9 @@ async function projectUsesImageBuiltins(root) {
11575
11638
  export {
11576
11639
  CSRF_COOKIE,
11577
11640
  DEFAULT_IMAGE_WARMUP_BIND_BUDGET_MS,
11641
+ DEFAULT_PAGE_WARMUP_BUDGET_MS,
11578
11642
  MODULE_BUNDLE_PATH,
11643
+ PAGE_WARMUP_HEADER,
11579
11644
  ROUTE_PARAMS_PLACEHOLDER,
11580
11645
  SHARED_SCRIPT_PATH,
11581
11646
  boundedCache,
@@ -11602,6 +11667,7 @@ export {
11602
11667
  resolveDomainElsewhere,
11603
11668
  resolveDomainRoute,
11604
11669
  resolveImageWarmupBindBudget,
11670
+ resolvePageWarmup,
11605
11671
  resolveServeOpenUrl,
11606
11672
  responseTakesCsrfCookie,
11607
11673
  routeSpecificity,