bun-plugin-stx 0.2.409 → 0.2.411
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/domain-routes.d.ts +88 -0
- package/dist/serve.d.ts +5 -0
- package/dist/serve.js +124 -0
- package/package.json +3 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile `{ pattern: path }` into routes, most specific first. Invalid
|
|
3
|
+
* entries are dropped with a warning rather than thrown: a malformed domain
|
|
4
|
+
* route must not stop a site booting.
|
|
5
|
+
*/
|
|
6
|
+
export declare function compileDomainRoutes(input: DomainRoutesOption | undefined, warn?: (message: string) => void): DomainRoute[];
|
|
7
|
+
/** The hostname a request was made to, without the port, lowercased. */
|
|
8
|
+
export declare function requestHost(req: Request): string;
|
|
9
|
+
/** The params a host captures for a route, or null when it does not match. */
|
|
10
|
+
export declare function matchDomain(host: string, route: DomainRoute): Record<string, string> | null;
|
|
11
|
+
/**
|
|
12
|
+
* The path a request to `host` at `pathname` should render, or null when no
|
|
13
|
+
* domain route applies (a host that matches nothing, or a path other than
|
|
14
|
+
* the root).
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveDomainRoute(host: string, pathname: string, routes: DomainRoute[]): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Where a request for another page on a routed host belongs, or null to
|
|
19
|
+
* answer it in place.
|
|
20
|
+
*
|
|
21
|
+
* Only for a route that names `elsewhere`, only for a host that matches it,
|
|
22
|
+
* only for paths other than the root, and only for page requests: `isAsset`
|
|
23
|
+
* is the server's own test for scripts, styles, `/api` and `public/` files,
|
|
24
|
+
* which stay where the page at the root can load them.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveDomainElsewhere(host: string, pathname: string, search: string, routes: DomainRoute[], isAsset: (pathname: string) => boolean): string | null;
|
|
27
|
+
/** One line for boot output: `{username}.example.com/ → /{username}`. */
|
|
28
|
+
export declare function describeDomainRoutes(routes: DomainRoute[]): string;
|
|
29
|
+
export declare interface DomainRoute {
|
|
30
|
+
pattern: string
|
|
31
|
+
labels: Label[]
|
|
32
|
+
to: string
|
|
33
|
+
elsewhere?: string
|
|
34
|
+
literals: number
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Domain routes: a host whose home page is a page of the site.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* serve({
|
|
41
|
+
* domains: {
|
|
42
|
+
* '{username}.example.com': '/{username}',
|
|
43
|
+
* 'shop.example.com': '/store',
|
|
44
|
+
* },
|
|
45
|
+
* })
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* `chris.example.com/` then renders exactly what `example.com/chris` renders
|
|
49
|
+
* — the same file route, the same `route.params.username` — and the address
|
|
50
|
+
* bar keeps saying `chris.example.com`. It is how a profile, a team or a shop
|
|
51
|
+
* gets a domain of its own on a site that is otherwise one app, the way a
|
|
52
|
+
* GitHub Pages or a Substack subdomain works.
|
|
53
|
+
*
|
|
54
|
+
* Only the host's root is routed. Every other path on that host — scripts,
|
|
55
|
+
* styles, images, `/api`, the other pages a navigation bar links to — is the
|
|
56
|
+
* site's own, answered as it would be on the main domain, so one page can be
|
|
57
|
+
* given a domain without the rest of the site breaking under it.
|
|
58
|
+
*
|
|
59
|
+
* Unless the route says where the rest of the site lives:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* domains: {
|
|
63
|
+
* '{username}.example.com': { to: '/{username}', elsewhere: 'https://example.com' },
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* Then a request for any other PAGE on that host is redirected there
|
|
68
|
+
* (`chris.example.com/pricing` → `example.com/pricing`), while its assets,
|
|
69
|
+
* `/api` and `public/` files are still answered in place, because the page
|
|
70
|
+
* at its root needs them. A browser keeps a session per origin: without
|
|
71
|
+
* this, signing in from a profile's own domain signs in to that domain, and
|
|
72
|
+
* the visitor ends up using the whole app under someone else's name.
|
|
73
|
+
*
|
|
74
|
+
* A pattern is a hostname whose labels are either literal or `{name}`, which
|
|
75
|
+
* matches exactly one DNS label (`{username}.example.com` matches
|
|
76
|
+
* `chris.example.com`, not `a.b.example.com` and not the bare apex). Literal
|
|
77
|
+
* patterns win over parameterised ones, then the one with more literal
|
|
78
|
+
* labels, so `docs.example.com` can be carved out of `{username}.example.com`.
|
|
79
|
+
* A parameter's value is placed in the target path URL-encoded.
|
|
80
|
+
*
|
|
81
|
+
* The host is read from `X-Forwarded-Host` before `Host`, because behind a
|
|
82
|
+
* gateway the latter is the upstream's address. Trusting it is safe here: a
|
|
83
|
+
* forged value can only choose which page of this same site renders for the
|
|
84
|
+
* root path, which the requester could have asked for by its own path.
|
|
85
|
+
*/
|
|
86
|
+
/** One label of a compiled pattern. */
|
|
87
|
+
declare type Label = { literal: string } | { param: string }
|
|
88
|
+
export type DomainRoutesOption = Record<string, string | { to: string, elsewhere?: string }>;
|
package/dist/serve.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { serve as bunServe } from 'bun';
|
|
2
|
+
import { compileDomainRoutes, describeDomainRoutes, requestHost, resolveDomainElsewhere, resolveDomainRoute } from './domain-routes';
|
|
3
|
+
import type { DomainRoutesOption } from './domain-routes';
|
|
2
4
|
import type { MiddlewareContext, MiddlewareHandler, MiddlewareRequest, PageMiddleware, PrepareMiddlewareRequest } from './page-middleware';
|
|
5
|
+
export type { DomainRoute, DomainRoutesOption } from './domain-routes';
|
|
3
6
|
export type { MiddlewareContext, MiddlewareDefinition, MiddlewareHandler, MiddlewareRequest, PageMiddleware, PrepareMiddlewareRequest } from './page-middleware';
|
|
4
7
|
/**
|
|
5
8
|
* Extract real top-level `<script server>` elements without matching tag-like
|
|
@@ -358,6 +361,7 @@ export declare interface ServeOptions {
|
|
|
358
361
|
| null
|
|
359
362
|
| undefined
|
|
360
363
|
| Promise<Response | Record<string, unknown> | null | undefined>
|
|
364
|
+
domains?: DomainRoutesOption
|
|
361
365
|
onResponse?: (req: Request, response: Response) =>
|
|
362
366
|
| Response
|
|
363
367
|
| null
|
|
@@ -388,3 +392,4 @@ export declare interface ServeOptions {
|
|
|
388
392
|
declare type CssEngine = Pick<typeof import('@stacksjs/ts-css/engine'), 'CSSGenerator' | 'config'>;
|
|
389
393
|
/** The Bun server `serve()` started - handed to `onRequest` beside each request. */
|
|
390
394
|
export type ServeServer = ReturnType<typeof bunServe>;
|
|
395
|
+
export { compileDomainRoutes, describeDomainRoutes, matchDomain, requestHost, resolveDomainElsewhere, resolveDomainRoute } from './domain-routes';
|
package/dist/serve.js
CHANGED
|
@@ -8723,6 +8723,107 @@ import { buildCodeFrame, locateFailureLine } from "@stacksjs/stx/build-message";
|
|
|
8723
8723
|
import { clearBundleFailures, getBundleFailures } from "@stacksjs/stx/client-script-bundler";
|
|
8724
8724
|
import { actionRedirectResponse, compressResponse, runPageAction as sharedRunPageAction } from "@stacksjs/stx";
|
|
8725
8725
|
|
|
8726
|
+
// src/domain-routes.ts
|
|
8727
|
+
var LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
8728
|
+
var PARAM = /^\{([A-Za-z_]\w*)\}$/;
|
|
8729
|
+
function compileDomainRoutes(input, warn = () => {}) {
|
|
8730
|
+
const routes = [];
|
|
8731
|
+
for (const [rawPattern, target] of Object.entries(input ?? {})) {
|
|
8732
|
+
const pattern = String(rawPattern).trim().toLowerCase().replace(/\.$/, "");
|
|
8733
|
+
const rawTo = typeof target === "object" && target ? target.to : target;
|
|
8734
|
+
const to = String(rawTo ?? "").trim();
|
|
8735
|
+
let elsewhere;
|
|
8736
|
+
if (typeof target === "object" && target?.elsewhere) {
|
|
8737
|
+
try {
|
|
8738
|
+
const url = new URL(String(target.elsewhere));
|
|
8739
|
+
if (url.protocol === "https:" || url.protocol === "http:")
|
|
8740
|
+
elsewhere = url.origin;
|
|
8741
|
+
} catch {}
|
|
8742
|
+
if (!elsewhere) {
|
|
8743
|
+
warn(`[stx] domain route "${rawPattern}" ignored: elsewhere "${target.elsewhere}" is not an http(s) origin`);
|
|
8744
|
+
continue;
|
|
8745
|
+
}
|
|
8746
|
+
}
|
|
8747
|
+
const parts = pattern.split(".");
|
|
8748
|
+
if (parts.length < 2 || !to.startsWith("/")) {
|
|
8749
|
+
warn(`[stx] domain route "${rawPattern}" -> "${rawTo}" ignored: a route is a hostname with a dot and a path starting with "/"`);
|
|
8750
|
+
continue;
|
|
8751
|
+
}
|
|
8752
|
+
const labels = [];
|
|
8753
|
+
const params = new Set;
|
|
8754
|
+
let valid = true;
|
|
8755
|
+
for (const part of parts) {
|
|
8756
|
+
const param = PARAM.exec(part);
|
|
8757
|
+
if (param) {
|
|
8758
|
+
labels.push({ param: param[1] });
|
|
8759
|
+
params.add(param[1]);
|
|
8760
|
+
} else if (LABEL.test(part)) {
|
|
8761
|
+
labels.push({ literal: part });
|
|
8762
|
+
} else {
|
|
8763
|
+
valid = false;
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
const missing = [...to.matchAll(/\{(\w+)\}/g)].map((match) => match[1]).filter((name) => !params.has(name));
|
|
8767
|
+
if (!valid || missing.length) {
|
|
8768
|
+
warn(`[stx] domain route "${rawPattern}" -> "${rawTo}" ignored: ${!valid ? "a label is neither a hostname label nor {name}" : `the path uses {${missing.join("}, {")}} which the hostname does not capture`}`);
|
|
8769
|
+
continue;
|
|
8770
|
+
}
|
|
8771
|
+
routes.push({ pattern, labels, to, ...elsewhere ? { elsewhere } : {}, literals: labels.filter((label) => ("literal" in label)).length });
|
|
8772
|
+
}
|
|
8773
|
+
return routes.sort((a, b) => b.literals - a.literals || b.labels.length - a.labels.length);
|
|
8774
|
+
}
|
|
8775
|
+
function requestHost(req) {
|
|
8776
|
+
const forwarded = req.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
8777
|
+
const host = forwarded || req.headers.get("host") || new URL(req.url).host;
|
|
8778
|
+
return host.replace(/:\d+$/, "").replace(/\.$/, "").toLowerCase();
|
|
8779
|
+
}
|
|
8780
|
+
function matchDomain(host, route) {
|
|
8781
|
+
const parts = host.split(".");
|
|
8782
|
+
if (parts.length !== route.labels.length)
|
|
8783
|
+
return null;
|
|
8784
|
+
const params = {};
|
|
8785
|
+
for (let i = 0;i < parts.length; i++) {
|
|
8786
|
+
const label = route.labels[i];
|
|
8787
|
+
const part = parts[i];
|
|
8788
|
+
if ("literal" in label) {
|
|
8789
|
+
if (label.literal !== part)
|
|
8790
|
+
return null;
|
|
8791
|
+
} else {
|
|
8792
|
+
if (!LABEL.test(part))
|
|
8793
|
+
return null;
|
|
8794
|
+
params[label.param] = part;
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
return params;
|
|
8798
|
+
}
|
|
8799
|
+
function resolveDomainRoute(host, pathname, routes) {
|
|
8800
|
+
if (!routes.length)
|
|
8801
|
+
return null;
|
|
8802
|
+
if (pathname !== "/" && pathname !== "" && pathname !== "/index")
|
|
8803
|
+
return null;
|
|
8804
|
+
for (const route of routes) {
|
|
8805
|
+
const params = matchDomain(host, route);
|
|
8806
|
+
if (!params)
|
|
8807
|
+
continue;
|
|
8808
|
+
return route.to.replace(/\{(\w+)\}/g, (_, name) => encodeURIComponent(params[name] ?? ""));
|
|
8809
|
+
}
|
|
8810
|
+
return null;
|
|
8811
|
+
}
|
|
8812
|
+
function resolveDomainElsewhere(host, pathname, search, routes, isAsset) {
|
|
8813
|
+
if (!routes.length || pathname === "/" || pathname === "" || pathname === "/index")
|
|
8814
|
+
return null;
|
|
8815
|
+
for (const route of routes) {
|
|
8816
|
+
if (!matchDomain(host, route))
|
|
8817
|
+
continue;
|
|
8818
|
+
if (!route.elsewhere || isAsset(pathname))
|
|
8819
|
+
return null;
|
|
8820
|
+
return `${route.elsewhere}${pathname}${search}`;
|
|
8821
|
+
}
|
|
8822
|
+
return null;
|
|
8823
|
+
}
|
|
8824
|
+
function describeDomainRoutes(routes) {
|
|
8825
|
+
return routes.map((route) => `${route.pattern}/ \u2192 ${route.to}${route.elsewhere ? ` (other pages: ${route.elsewhere})` : ""}`).join(", ");
|
|
8826
|
+
}
|
|
8726
8827
|
// src/page-middleware.ts
|
|
8727
8828
|
var DEFAULT_PRIORITY = 10;
|
|
8728
8829
|
function middlewarePriority(middleware) {
|
|
@@ -9301,6 +9402,9 @@ async function serve(options) {
|
|
|
9301
9402
|
const fallbackComponentsDir = options.fallbackComponentsDir ?? stxConfig.fallbackComponentsDir;
|
|
9302
9403
|
const partialsDir = options.partialsDir ?? stxConfig.partialsDir ?? defaultStxConfig.partialsDir;
|
|
9303
9404
|
const publicDir = options.publicDir ?? stxConfig.publicDir ?? "public";
|
|
9405
|
+
const domainRoutes = compileDomainRoutes(options.domains ?? stxConfig.server?.domains, (message) => console.warn(message));
|
|
9406
|
+
if (domainRoutes.length)
|
|
9407
|
+
console.log(`[stx] domain routes: ${describeDomainRoutes(domainRoutes)}`);
|
|
9304
9408
|
let markPlaceholdersReady = () => {};
|
|
9305
9409
|
const placeholdersReady = new Promise((resolve13) => {
|
|
9306
9410
|
markPlaceholdersReady = resolve13;
|
|
@@ -10490,6 +10594,20 @@ ${block}`;
|
|
|
10490
10594
|
if (warmup)
|
|
10491
10595
|
await warmup;
|
|
10492
10596
|
return compressResponse(req, await (async () => {
|
|
10597
|
+
if (domainRoutes.length) {
|
|
10598
|
+
const original = new URL(req.url);
|
|
10599
|
+
const routed = resolveDomainRoute(requestHost(req), original.pathname, domainRoutes);
|
|
10600
|
+
if (routed) {
|
|
10601
|
+
original.pathname = routed;
|
|
10602
|
+
const headers = new Headers(req.headers);
|
|
10603
|
+
headers.set("x-stx-domain-route", requestHost(req));
|
|
10604
|
+
req = new Request(original, { headers, method: req.method, body: req.body, redirect: req.redirect, duplex: "half" });
|
|
10605
|
+
} else if (req.method === "GET" || req.method === "HEAD") {
|
|
10606
|
+
const location = resolveDomainElsewhere(requestHost(req), original.pathname, original.search, domainRoutes, (pathname) => pathname.startsWith("/api/") || pathname.startsWith("/_stx/") || pathname.startsWith("/.well-known/") || isStaticAssetPath(pathname) || publicFileExists(pathname, publicDir));
|
|
10607
|
+
if (location)
|
|
10608
|
+
return new Response(null, { status: 301, headers: { Location: location } });
|
|
10609
|
+
}
|
|
10610
|
+
}
|
|
10493
10611
|
const url = new URL(req.url);
|
|
10494
10612
|
let path = url.pathname;
|
|
10495
10613
|
const proxyRules = stxConfig.server?.proxy;
|
|
@@ -11378,20 +11496,26 @@ export {
|
|
|
11378
11496
|
responseTakesCsrfCookie,
|
|
11379
11497
|
resolveServeOpenUrl,
|
|
11380
11498
|
resolveImageWarmupBindBudget,
|
|
11499
|
+
resolveDomainRoute,
|
|
11500
|
+
resolveDomainElsewhere,
|
|
11501
|
+
requestHost,
|
|
11381
11502
|
render404Page,
|
|
11382
11503
|
publicFileExists,
|
|
11383
11504
|
projectUsesImageBuiltins,
|
|
11384
11505
|
pageRootOf,
|
|
11385
11506
|
pageExcluder,
|
|
11386
11507
|
orderDynamicRouteFiles,
|
|
11508
|
+
matchDomain,
|
|
11387
11509
|
isStaticAssetPath,
|
|
11388
11510
|
isRenderableCacheCandidate,
|
|
11389
11511
|
isRenderFailure,
|
|
11390
11512
|
isProductionServe,
|
|
11391
11513
|
fillRouteParams,
|
|
11392
11514
|
extractServerScriptsFromTemplate,
|
|
11515
|
+
describeDomainRoutes,
|
|
11393
11516
|
csrfTokenToMint,
|
|
11394
11517
|
csrfCookieHeader,
|
|
11518
|
+
compileDomainRoutes,
|
|
11395
11519
|
bundleBrowserAsset,
|
|
11396
11520
|
buildDynamicRouteRegexes,
|
|
11397
11521
|
boundedCache,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-plugin-stx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.411",
|
|
5
5
|
"description": "A Bun plugin that allows for using Laravel Blade-like syntax.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"typecheck": "bun tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@stacksjs/stx": "0.2.
|
|
78
|
-
"stx-router": "0.2.
|
|
77
|
+
"@stacksjs/stx": "0.2.411",
|
|
78
|
+
"stx-router": "0.2.411"
|
|
79
79
|
}
|
|
80
80
|
}
|