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.d.ts +22 -0
- package/dist/serve.js +755 -689
- package/package.json +3 -3
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
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
return
|
|
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
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
}
|
|
10710
|
-
}
|
|
10711
|
-
|
|
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
|
-
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
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
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
}
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
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
|
-
|
|
10773
|
-
|
|
10782
|
+
if (currentBuildErrors.length > 0) {
|
|
10783
|
+
c.enqueue(hmrEncoder.encode(`data: ${JSON.stringify({ type: "build-error", errors: currentBuildErrors })}
|
|
10774
10784
|
|
|
10775
10785
|
`));
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10786
|
+
}
|
|
10787
|
+
keepalive = setInterval(() => {
|
|
10788
|
+
try {
|
|
10789
|
+
c.enqueue(hmrEncoder.encode(`: keepalive
|
|
10780
10790
|
|
|
10781
10791
|
`));
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
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
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
const
|
|
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":
|
|
10829
|
-
ETag: etag,
|
|
10893
|
+
"Cache-Control": "no-store",
|
|
10830
10894
|
...corsHeaders
|
|
10831
|
-
}
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
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
|
-
|
|
10839
|
-
|
|
10955
|
+
}
|
|
10956
|
+
if (path.startsWith("/_stx/component/")) {
|
|
10957
|
+
const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
|
|
10958
|
+
if (componentName) {
|
|
10840
10959
|
try {
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
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
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
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
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
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
|
-
|
|
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
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
|
|
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 (
|
|
10983
|
-
|
|
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
|
|
10989
|
-
const
|
|
10990
|
-
const
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
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
|
-
|
|
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
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
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
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
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
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
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
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
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
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
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
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
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":
|
|
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 (
|
|
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
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
const
|
|
11262
|
-
|
|
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":
|
|
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
|
-
|
|
11307
|
+
continue;
|
|
11278
11308
|
}
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
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
|
|
11284
|
-
const file = Bun.file(filePath);
|
|
11324
|
+
const file = Bun.file(resolvedPath);
|
|
11285
11325
|
if (await file.exists()) {
|
|
11286
|
-
const
|
|
11287
|
-
if (
|
|
11288
|
-
return
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
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
|
-
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
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
|
-
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
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
|
-
|
|
11331
|
-
|
|
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
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
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
|
-
|
|
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,
|