@stacksjs/server 0.72.91 → 0.72.93

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/proxy.d.ts CHANGED
@@ -15,6 +15,16 @@ export declare function resolveApiProxyRules(input?: ApiProxyConfig): ApiProxyRu
15
15
  * configuration pass it in.
16
16
  */
17
17
  export declare function isApiBoundRequest(req: Request, pathname: string, rules?: ApiProxyRules): boolean;
18
+ /**
19
+ * The same decision, without a `Request` to ask.
20
+ *
21
+ * Boot-time checks want to know whether a route the API registered can ever
22
+ * be reached through the views server, and constructing a `Request` per route
23
+ * to find out would be silly. More to the point, a second implementation of
24
+ * this rule would drift from the one that actually routes traffic, and a
25
+ * diagnostic that disagrees with the thing it describes is worse than none.
26
+ */
27
+ export declare function isApiBoundPath(method: string, pathname: string, rules?: ApiProxyRules): boolean;
18
28
  /**
19
29
  * A one-line summary of what will be forwarded, for the dev server's boot
20
30
  * output.
@@ -24,6 +34,25 @@ export declare function isApiBoundRequest(req: Request, pathname: string, rules?
24
34
  * invisible.
25
35
  */
26
36
  export declare function describeApiProxyRules(rules: ApiProxyRules): string;
37
+ /**` and the mutating verbs go by
38
+ * default, which is why `route.post('/subscribe', …)` at the root works and
39
+ * `route.get('/sitemap.xml', …)` does not: the GET is not forwarded, stx
40
+ * renders it as a page, finds no page, and answers its own 404. The handler
41
+ * never runs and nothing says so (stacksjs/stacks#2326).
42
+ *
43
+ * Give this the app's own root-mounted routes, not the whole table. The
44
+ * framework registers around a hundred non-`/api` GETs that are reached in
45
+ * topologies of their own, and listing those buries the one that is broken.
46
+ */
47
+ export declare function unforwardableRoutes<T extends RoutePathLike>(routes: readonly T[], rules: ApiProxyRules): T[];
48
+ /**
49
+ * The boot warning for routes that cannot be reached, naming the fix.
50
+ *
51
+ * The silence is the part the report called worst: the route is declared, it
52
+ * reads correctly in review, and it 404s in production the same way a typo
53
+ * would.
54
+ */
55
+ export declare function describeUnforwardableRoutes(routes: readonly (RoutePathLike & { file?: string })[]): string;
27
56
  export declare function proxyToBackend(req: Request, backendBase: string, stripPrefix?: string): Promise<Response>;
28
57
  /**` traffic goes, or null when there is no safe answer.
29
58
  *
@@ -93,3 +122,7 @@ export declare interface ApiProxyConfig {
93
122
  paths?: readonly string[]
94
123
  methods?: readonly string[]
95
124
  }
125
+ export declare interface RoutePathLike {
126
+ method: string
127
+ path: string
128
+ }
package/dist/proxy.js CHANGED
@@ -1 +1,2 @@
1
- export const DEFAULT_API_PREFIX="/api/",DEFAULT_API_METHODS=["POST","PUT","PATCH","DELETE"];export function resolveApiProxyRules(input={}){const prefixes=[DEFAULT_API_PREFIX];for(const raw of input.prefixes??[]){const prefix=String(raw).trim();if(!prefix||!prefix.startsWith("/"))continue;const normalized=prefix.endsWith("/")?prefix:`${prefix}/`;if(!prefixes.includes(normalized))prefixes.push(normalized)}const paths=[];for(const raw of input.paths??[]){const path=String(raw).trim();if(!path||!path.startsWith("/"))continue;const normalized=path.length>1&&path.endsWith("/")?path.slice(0,-1):path;if(!paths.includes(normalized))paths.push(normalized)}const methods=new Set((input.methods??DEFAULT_API_METHODS).map((method)=>String(method).toUpperCase()));return{prefixes,paths,methods}}function matchesPrefix(pathname,prefix){return pathname.startsWith(prefix)||pathname===prefix.slice(0,-1)}export function isApiBoundRequest(req,pathname,rules){if(!rules)return pathname.startsWith(DEFAULT_API_PREFIX)||DEFAULT_API_METHODS.includes(req.method);if(rules.methods.has(req.method))return!0;if(rules.paths.includes(pathname))return!0;return rules.prefixes.some((prefix)=>matchesPrefix(pathname,prefix))}export function describeApiProxyRules(rules){const parts=[`prefixes ${rules.prefixes.join(" ")}`,`methods ${[...rules.methods].sort().join("/")}`];if(rules.paths.length>0)parts.push(`paths ${rules.paths.join(" ")}`);return parts.join(", ")}export async function proxyToBackend(req,backendBase,stripPrefix){const incoming=new URL(req.url);let pathname=incoming.pathname;if(stripPrefix&&(pathname===stripPrefix||pathname.startsWith(`${stripPrefix}/`)))pathname=pathname.slice(stripPrefix.length)||"/";const target=`${backendBase}${pathname}${incoming.search}`,fwd=new Headers(req.headers);fwd.delete("host");fwd.delete("content-length");fwd.set("x-forwarded-host",incoming.host);fwd.set("x-forwarded-proto",incoming.protocol.replace(":",""));const body=req.method==="GET"||req.method==="HEAD"?void 0:await req.arrayBuffer(),upstream=await fetch(target,{method:req.method,headers:fwd,body,redirect:"manual"}),out=new Headers(upstream.headers);out.delete("content-length");out.delete("content-encoding");return new Response(upstream.body,{status:upstream.status,statusText:upstream.statusText,headers:out})}export function resolveApiBase(configuredPort,env=process.env){if(env.API_URL)return env.API_URL;const explicitPort=Number(env.PORT_API);if(explicitPort)return`http://127.0.0.1:${explicitPort}`;if(["production","staging","development"].includes((env.APP_ENV||"").toLowerCase()))return null;return`http://127.0.0.1:${configuredPort||3008}`}
1
+ export const DEFAULT_API_PREFIX="/api/",DEFAULT_API_METHODS=["POST","PUT","PATCH","DELETE"];export function resolveApiProxyRules(input={}){const prefixes=[DEFAULT_API_PREFIX];for(const raw of input.prefixes??[]){const prefix=String(raw).trim();if(!prefix||!prefix.startsWith("/"))continue;const normalized=prefix.endsWith("/")?prefix:`${prefix}/`;if(!prefixes.includes(normalized))prefixes.push(normalized)}const paths=[];for(const raw of input.paths??[]){const path=String(raw).trim();if(!path||!path.startsWith("/"))continue;const normalized=path.length>1&&path.endsWith("/")?path.slice(0,-1):path;if(!paths.includes(normalized))paths.push(normalized)}const methods=new Set((input.methods??DEFAULT_API_METHODS).map((method)=>String(method).toUpperCase()));return{prefixes,paths,methods}}function matchesPrefix(pathname,prefix){return pathname.startsWith(prefix)||pathname===prefix.slice(0,-1)}export function isApiBoundRequest(req,pathname,rules){return isApiBoundPath(req.method,pathname,rules)}export function isApiBoundPath(method,pathname,rules){const verb=method.toUpperCase();if(!rules)return pathname.startsWith(DEFAULT_API_PREFIX)||DEFAULT_API_METHODS.includes(verb);if(rules.methods.has(verb))return!0;if(rules.paths.includes(pathname))return!0;return rules.prefixes.some((prefix)=>matchesPrefix(pathname,prefix))}export function describeApiProxyRules(rules){const parts=[`prefixes ${rules.prefixes.join(" ")}`,`methods ${[...rules.methods].sort().join("/")}`];if(rules.paths.length>0)parts.push(`paths ${rules.paths.join(" ")}`);return parts.join(", ")}export function unforwardableRoutes(routes,rules){return routes.filter((route)=>!isApiBoundPath(route.method,route.path,rules))}export function describeUnforwardableRoutes(routes){const lines=routes.map((route)=>{const origin=route.file?` (${route.file})`:"";return` ${route.method} ${route.path}${origin}`});return[`${routes.length} root-mounted route${routes.length===1?"":"s"} the views server will not forward, so ${routes.length===1?"it":"they"} will answer the view 404 rather than reaching the handler:`,...lines,"Add them to `proxy.paths` in config/server.ts (or their prefix to `proxy.prefixes`) so the views server forwards them to the API."].join(`
2
+ `)}export async function proxyToBackend(req,backendBase,stripPrefix){const incoming=new URL(req.url);let pathname=incoming.pathname;if(stripPrefix&&(pathname===stripPrefix||pathname.startsWith(`${stripPrefix}/`)))pathname=pathname.slice(stripPrefix.length)||"/";const target=`${backendBase}${pathname}${incoming.search}`,fwd=new Headers(req.headers);fwd.delete("host");fwd.delete("content-length");fwd.set("x-forwarded-host",incoming.host);fwd.set("x-forwarded-proto",incoming.protocol.replace(":",""));const body=req.method==="GET"||req.method==="HEAD"?void 0:await req.arrayBuffer(),upstream=await fetch(target,{method:req.method,headers:fwd,body,redirect:"manual"}),out=new Headers(upstream.headers);out.delete("content-length");out.delete("content-encoding");return new Response(upstream.body,{status:upstream.status,statusText:upstream.statusText,headers:out})}export function resolveApiBase(configuredPort,env=process.env){if(env.API_URL)return env.API_URL;const explicitPort=Number(env.PORT_API);if(explicitPort)return`http://127.0.0.1:${explicitPort}`;if(["production","staging","development"].includes((env.APP_ENV||"").toLowerCase()))return null;return`http://127.0.0.1:${configuredPort||3008}`}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/server",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.91",
5
+ "version": "0.72.93",
6
6
  "description": "Local development and production-ready.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,11 +58,11 @@
58
58
  "prepublishOnly": "bun run build"
59
59
  },
60
60
  "devDependencies": {
61
- "@stacksjs/config": "0.72.91",
61
+ "@stacksjs/config": "0.72.93",
62
62
  "better-dx": "^0.2.24",
63
- "@stacksjs/path": "0.72.91",
64
- "@stacksjs/router": "0.72.91",
65
- "@stacksjs/validation": "0.72.91"
63
+ "@stacksjs/path": "0.72.93",
64
+ "@stacksjs/router": "0.72.93",
65
+ "@stacksjs/validation": "0.72.93"
66
66
  },
67
67
  "dependencies": {
68
68
  "bun-plugin-auto-imports": "^0.4.0"