bun-plugin-stx 0.2.409 → 0.2.410
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 +62 -0
- package/dist/serve.d.ts +5 -0
- package/dist/serve.js +94 -0
- package/package.json +3 -3
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
/** One line for boot output: `{username}.example.com/ → /{username}`. */
|
|
18
|
+
export declare function describeDomainRoutes(routes: DomainRoute[]): string;
|
|
19
|
+
export declare interface DomainRoute {
|
|
20
|
+
pattern: string
|
|
21
|
+
labels: Label[]
|
|
22
|
+
to: string
|
|
23
|
+
literals: number
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Domain routes: a host whose home page is a page of the site.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* serve({
|
|
30
|
+
* domains: {
|
|
31
|
+
* '{username}.example.com': '/{username}',
|
|
32
|
+
* 'shop.example.com': '/store',
|
|
33
|
+
* },
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* `chris.example.com/` then renders exactly what `example.com/chris` renders
|
|
38
|
+
* — the same file route, the same `route.params.username` — and the address
|
|
39
|
+
* bar keeps saying `chris.example.com`. It is how a profile, a team or a shop
|
|
40
|
+
* gets a domain of its own on a site that is otherwise one app, the way a
|
|
41
|
+
* GitHub Pages or a Substack subdomain works.
|
|
42
|
+
*
|
|
43
|
+
* Only the host's root is routed. Every other path on that host — scripts,
|
|
44
|
+
* styles, images, `/api`, the other pages a navigation bar links to — is the
|
|
45
|
+
* site's own, answered as it would be on the main domain, so one page can be
|
|
46
|
+
* given a domain without the rest of the site breaking under it.
|
|
47
|
+
*
|
|
48
|
+
* A pattern is a hostname whose labels are either literal or `{name}`, which
|
|
49
|
+
* matches exactly one DNS label (`{username}.example.com` matches
|
|
50
|
+
* `chris.example.com`, not `a.b.example.com` and not the bare apex). Literal
|
|
51
|
+
* patterns win over parameterised ones, then the one with more literal
|
|
52
|
+
* labels, so `docs.example.com` can be carved out of `{username}.example.com`.
|
|
53
|
+
* A parameter's value is placed in the target path URL-encoded.
|
|
54
|
+
*
|
|
55
|
+
* The host is read from `X-Forwarded-Host` before `Host`, because behind a
|
|
56
|
+
* gateway the latter is the upstream's address. Trusting it is safe here: a
|
|
57
|
+
* forged value can only choose which page of this same site renders for the
|
|
58
|
+
* root path, which the requester could have asked for by its own path.
|
|
59
|
+
*/
|
|
60
|
+
/** One label of a compiled pattern. */
|
|
61
|
+
declare type Label = { literal: string } | { param: string }
|
|
62
|
+
export type DomainRoutesOption = Record<string, string>;
|
package/dist/serve.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { serve as bunServe } from 'bun';
|
|
2
|
+
import { compileDomainRoutes, describeDomainRoutes, requestHost, 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, resolveDomainRoute } from './domain-routes';
|
package/dist/serve.js
CHANGED
|
@@ -8723,6 +8723,82 @@ 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, rawTo] of Object.entries(input ?? {})) {
|
|
8732
|
+
const pattern = String(rawPattern).trim().toLowerCase().replace(/\.$/, "");
|
|
8733
|
+
const to = String(rawTo ?? "").trim();
|
|
8734
|
+
const parts = pattern.split(".");
|
|
8735
|
+
if (parts.length < 2 || !to.startsWith("/")) {
|
|
8736
|
+
warn(`[stx] domain route "${rawPattern}" -> "${rawTo}" ignored: a route is a hostname with a dot and a path starting with "/"`);
|
|
8737
|
+
continue;
|
|
8738
|
+
}
|
|
8739
|
+
const labels = [];
|
|
8740
|
+
const params = new Set;
|
|
8741
|
+
let valid = true;
|
|
8742
|
+
for (const part of parts) {
|
|
8743
|
+
const param = PARAM.exec(part);
|
|
8744
|
+
if (param) {
|
|
8745
|
+
labels.push({ param: param[1] });
|
|
8746
|
+
params.add(param[1]);
|
|
8747
|
+
} else if (LABEL.test(part)) {
|
|
8748
|
+
labels.push({ literal: part });
|
|
8749
|
+
} else {
|
|
8750
|
+
valid = false;
|
|
8751
|
+
}
|
|
8752
|
+
}
|
|
8753
|
+
const missing = [...to.matchAll(/\{(\w+)\}/g)].map((match) => match[1]).filter((name) => !params.has(name));
|
|
8754
|
+
if (!valid || missing.length) {
|
|
8755
|
+
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`}`);
|
|
8756
|
+
continue;
|
|
8757
|
+
}
|
|
8758
|
+
routes.push({ pattern, labels, to, literals: labels.filter((label) => ("literal" in label)).length });
|
|
8759
|
+
}
|
|
8760
|
+
return routes.sort((a, b) => b.literals - a.literals || b.labels.length - a.labels.length);
|
|
8761
|
+
}
|
|
8762
|
+
function requestHost(req) {
|
|
8763
|
+
const forwarded = req.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
8764
|
+
const host = forwarded || req.headers.get("host") || new URL(req.url).host;
|
|
8765
|
+
return host.replace(/:\d+$/, "").replace(/\.$/, "").toLowerCase();
|
|
8766
|
+
}
|
|
8767
|
+
function matchDomain(host, route) {
|
|
8768
|
+
const parts = host.split(".");
|
|
8769
|
+
if (parts.length !== route.labels.length)
|
|
8770
|
+
return null;
|
|
8771
|
+
const params = {};
|
|
8772
|
+
for (let i = 0;i < parts.length; i++) {
|
|
8773
|
+
const label = route.labels[i];
|
|
8774
|
+
const part = parts[i];
|
|
8775
|
+
if ("literal" in label) {
|
|
8776
|
+
if (label.literal !== part)
|
|
8777
|
+
return null;
|
|
8778
|
+
} else {
|
|
8779
|
+
if (!LABEL.test(part))
|
|
8780
|
+
return null;
|
|
8781
|
+
params[label.param] = part;
|
|
8782
|
+
}
|
|
8783
|
+
}
|
|
8784
|
+
return params;
|
|
8785
|
+
}
|
|
8786
|
+
function resolveDomainRoute(host, pathname, routes) {
|
|
8787
|
+
if (!routes.length)
|
|
8788
|
+
return null;
|
|
8789
|
+
if (pathname !== "/" && pathname !== "" && pathname !== "/index")
|
|
8790
|
+
return null;
|
|
8791
|
+
for (const route of routes) {
|
|
8792
|
+
const params = matchDomain(host, route);
|
|
8793
|
+
if (!params)
|
|
8794
|
+
continue;
|
|
8795
|
+
return route.to.replace(/\{(\w+)\}/g, (_, name) => encodeURIComponent(params[name] ?? ""));
|
|
8796
|
+
}
|
|
8797
|
+
return null;
|
|
8798
|
+
}
|
|
8799
|
+
function describeDomainRoutes(routes) {
|
|
8800
|
+
return routes.map((route) => `${route.pattern}/ \u2192 ${route.to}`).join(", ");
|
|
8801
|
+
}
|
|
8726
8802
|
// src/page-middleware.ts
|
|
8727
8803
|
var DEFAULT_PRIORITY = 10;
|
|
8728
8804
|
function middlewarePriority(middleware) {
|
|
@@ -9301,6 +9377,9 @@ async function serve(options) {
|
|
|
9301
9377
|
const fallbackComponentsDir = options.fallbackComponentsDir ?? stxConfig.fallbackComponentsDir;
|
|
9302
9378
|
const partialsDir = options.partialsDir ?? stxConfig.partialsDir ?? defaultStxConfig.partialsDir;
|
|
9303
9379
|
const publicDir = options.publicDir ?? stxConfig.publicDir ?? "public";
|
|
9380
|
+
const domainRoutes = compileDomainRoutes(options.domains ?? stxConfig.server?.domains, (message) => console.warn(message));
|
|
9381
|
+
if (domainRoutes.length)
|
|
9382
|
+
console.log(`[stx] domain routes: ${describeDomainRoutes(domainRoutes)}`);
|
|
9304
9383
|
let markPlaceholdersReady = () => {};
|
|
9305
9384
|
const placeholdersReady = new Promise((resolve13) => {
|
|
9306
9385
|
markPlaceholdersReady = resolve13;
|
|
@@ -10490,6 +10569,16 @@ ${block}`;
|
|
|
10490
10569
|
if (warmup)
|
|
10491
10570
|
await warmup;
|
|
10492
10571
|
return compressResponse(req, await (async () => {
|
|
10572
|
+
if (domainRoutes.length) {
|
|
10573
|
+
const original = new URL(req.url);
|
|
10574
|
+
const routed = resolveDomainRoute(requestHost(req), original.pathname, domainRoutes);
|
|
10575
|
+
if (routed) {
|
|
10576
|
+
original.pathname = routed;
|
|
10577
|
+
const headers = new Headers(req.headers);
|
|
10578
|
+
headers.set("x-stx-domain-route", requestHost(req));
|
|
10579
|
+
req = new Request(original, { headers, method: req.method, body: req.body, redirect: req.redirect, duplex: "half" });
|
|
10580
|
+
}
|
|
10581
|
+
}
|
|
10493
10582
|
const url = new URL(req.url);
|
|
10494
10583
|
let path = url.pathname;
|
|
10495
10584
|
const proxyRules = stxConfig.server?.proxy;
|
|
@@ -11378,20 +11467,25 @@ export {
|
|
|
11378
11467
|
responseTakesCsrfCookie,
|
|
11379
11468
|
resolveServeOpenUrl,
|
|
11380
11469
|
resolveImageWarmupBindBudget,
|
|
11470
|
+
resolveDomainRoute,
|
|
11471
|
+
requestHost,
|
|
11381
11472
|
render404Page,
|
|
11382
11473
|
publicFileExists,
|
|
11383
11474
|
projectUsesImageBuiltins,
|
|
11384
11475
|
pageRootOf,
|
|
11385
11476
|
pageExcluder,
|
|
11386
11477
|
orderDynamicRouteFiles,
|
|
11478
|
+
matchDomain,
|
|
11387
11479
|
isStaticAssetPath,
|
|
11388
11480
|
isRenderableCacheCandidate,
|
|
11389
11481
|
isRenderFailure,
|
|
11390
11482
|
isProductionServe,
|
|
11391
11483
|
fillRouteParams,
|
|
11392
11484
|
extractServerScriptsFromTemplate,
|
|
11485
|
+
describeDomainRoutes,
|
|
11393
11486
|
csrfTokenToMint,
|
|
11394
11487
|
csrfCookieHeader,
|
|
11488
|
+
compileDomainRoutes,
|
|
11395
11489
|
bundleBrowserAsset,
|
|
11396
11490
|
buildDynamicRouteRegexes,
|
|
11397
11491
|
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.410",
|
|
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.410",
|
|
78
|
+
"stx-router": "0.2.410"
|
|
79
79
|
}
|
|
80
80
|
}
|