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.
@@ -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.409",
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.409",
78
- "stx-router": "0.2.409"
77
+ "@stacksjs/stx": "0.2.410",
78
+ "stx-router": "0.2.410"
79
79
  }
80
80
  }