@stacksjs/bun-router 0.0.23 → 0.0.25
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/cli.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +93 -1
- package/dist/response/compression.d.ts +89 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export * as Errors from './errors';
|
|
|
20
20
|
export * from './middleware';
|
|
21
21
|
export { enableRequestContext, getCurrentRequest, request, runWithRequest, setCurrentRequest } from './request/context';
|
|
22
22
|
export * from './session';
|
|
23
|
+
export * from './response/compression';
|
|
23
24
|
export * from './response/response-factory';
|
|
24
25
|
export * from './router';
|
|
25
26
|
export { MiddlewareGroupRegistry, middlewareGroups } from './router/middleware-groups';
|
package/dist/index.js
CHANGED
|
@@ -2047,6 +2047,91 @@ function request() {
|
|
|
2047
2047
|
}
|
|
2048
2048
|
return req;
|
|
2049
2049
|
}
|
|
2050
|
+
// src/response/compression.ts
|
|
2051
|
+
var DEFAULT_COMPRESSION = {
|
|
2052
|
+
enabled: true,
|
|
2053
|
+
level: 6,
|
|
2054
|
+
threshold: 1024
|
|
2055
|
+
};
|
|
2056
|
+
function isCompressible(contentType) {
|
|
2057
|
+
if (!contentType)
|
|
2058
|
+
return false;
|
|
2059
|
+
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
2060
|
+
if (type.startsWith("text/"))
|
|
2061
|
+
return true;
|
|
2062
|
+
return [
|
|
2063
|
+
"application/json",
|
|
2064
|
+
"application/ld+json",
|
|
2065
|
+
"application/manifest+json",
|
|
2066
|
+
"application/javascript",
|
|
2067
|
+
"application/x-javascript",
|
|
2068
|
+
"application/xml",
|
|
2069
|
+
"application/xhtml+xml",
|
|
2070
|
+
"application/rss+xml",
|
|
2071
|
+
"application/atom+xml",
|
|
2072
|
+
"application/graphql",
|
|
2073
|
+
"application/x-ndjson",
|
|
2074
|
+
"image/svg+xml"
|
|
2075
|
+
].includes(type);
|
|
2076
|
+
}
|
|
2077
|
+
function negotiateEncoding(header) {
|
|
2078
|
+
if (!header)
|
|
2079
|
+
return null;
|
|
2080
|
+
const offers = new Map;
|
|
2081
|
+
for (const part of header.split(",")) {
|
|
2082
|
+
const [name = "", ...parameters] = part.trim().split(";");
|
|
2083
|
+
const quality = parameters.map((parameter) => /^\s*q=([\d.]+)\s*$/i.exec(parameter)).find(Boolean);
|
|
2084
|
+
offers.set(name.trim().toLowerCase(), quality ? Number(quality[1]) : 1);
|
|
2085
|
+
}
|
|
2086
|
+
const wanted = (name) => (offers.get(name) ?? offers.get("*") ?? 0) > 0;
|
|
2087
|
+
if (wanted("gzip"))
|
|
2088
|
+
return "gzip";
|
|
2089
|
+
if (wanted("deflate"))
|
|
2090
|
+
return "deflate";
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
function shouldCompress(response, encoding, threshold) {
|
|
2094
|
+
if (!encoding)
|
|
2095
|
+
return false;
|
|
2096
|
+
if (response.status === 204 || response.status === 304 || response.status === 206)
|
|
2097
|
+
return false;
|
|
2098
|
+
if (response.headers.has("content-encoding"))
|
|
2099
|
+
return false;
|
|
2100
|
+
if (!isCompressible(response.headers.get("content-type")))
|
|
2101
|
+
return false;
|
|
2102
|
+
if (!response.body)
|
|
2103
|
+
return false;
|
|
2104
|
+
const length = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
2105
|
+
return Number.isFinite(length) ? length >= threshold : true;
|
|
2106
|
+
}
|
|
2107
|
+
async function compressResponse(response, request2, options = {}) {
|
|
2108
|
+
const settings = { ...DEFAULT_COMPRESSION, ...options };
|
|
2109
|
+
if (!settings.enabled)
|
|
2110
|
+
return response;
|
|
2111
|
+
const encoding = negotiateEncoding(request2.headers.get("accept-encoding"));
|
|
2112
|
+
if (!shouldCompress(response, encoding, settings.threshold)) {
|
|
2113
|
+
if (isCompressible(response.headers.get("content-type")) && !response.headers.has("content-encoding"))
|
|
2114
|
+
appendVary(response.headers, "Accept-Encoding");
|
|
2115
|
+
return response;
|
|
2116
|
+
}
|
|
2117
|
+
const headers = new Headers(response.headers);
|
|
2118
|
+
headers.set("Content-Encoding", encoding);
|
|
2119
|
+
appendVary(headers, "Accept-Encoding");
|
|
2120
|
+
headers.delete("Content-Length");
|
|
2121
|
+
const compressed = response.body.pipeThrough(new CompressionStream(encoding === "gzip" ? "gzip" : "deflate"));
|
|
2122
|
+
return new Response(compressed, { status: response.status, statusText: response.statusText, headers });
|
|
2123
|
+
}
|
|
2124
|
+
function appendVary(headers, value) {
|
|
2125
|
+
const existing = headers.get("vary");
|
|
2126
|
+
if (!existing) {
|
|
2127
|
+
headers.set("Vary", value);
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
const parts = existing.split(",").map((part) => part.trim().toLowerCase());
|
|
2131
|
+
if (parts.includes("*") || parts.includes(value.toLowerCase()))
|
|
2132
|
+
return;
|
|
2133
|
+
headers.set("Vary", `${existing}, ${value}`);
|
|
2134
|
+
}
|
|
2050
2135
|
// src/response/response-factory.ts
|
|
2051
2136
|
var response = {
|
|
2052
2137
|
json: (data, options = {}) => {
|
|
@@ -9563,7 +9648,8 @@ function registerServerHandling(RouterClass) {
|
|
|
9563
9648
|
handleRequest: {
|
|
9564
9649
|
async value(req) {
|
|
9565
9650
|
const self = this;
|
|
9566
|
-
|
|
9651
|
+
const response2 = await runWithRequest(req, () => self.handleRequestImpl(req));
|
|
9652
|
+
return await compressResponse(response2, req, self.config?.compression ?? {});
|
|
9567
9653
|
},
|
|
9568
9654
|
writable: true,
|
|
9569
9655
|
configurable: true
|
|
@@ -13664,6 +13750,7 @@ export {
|
|
|
13664
13750
|
spyOnMiddleware,
|
|
13665
13751
|
singleFileUpload,
|
|
13666
13752
|
signRequest,
|
|
13753
|
+
shouldCompress,
|
|
13667
13754
|
setCurrentRequest,
|
|
13668
13755
|
setContainer,
|
|
13669
13756
|
session,
|
|
@@ -13695,6 +13782,7 @@ export {
|
|
|
13695
13782
|
parseThrottleString,
|
|
13696
13783
|
oauth2Auth,
|
|
13697
13784
|
normalizePath,
|
|
13785
|
+
negotiateEncoding,
|
|
13698
13786
|
multipleFileUpload,
|
|
13699
13787
|
modelMocks,
|
|
13700
13788
|
mockMiddleware,
|
|
@@ -13706,6 +13794,7 @@ export {
|
|
|
13706
13794
|
jsonBody,
|
|
13707
13795
|
joinPaths,
|
|
13708
13796
|
isRouteHandler,
|
|
13797
|
+
isCompressible,
|
|
13709
13798
|
isActionClass,
|
|
13710
13799
|
inputValidation,
|
|
13711
13800
|
injectQueryPreservationScript,
|
|
@@ -13763,6 +13852,7 @@ export {
|
|
|
13763
13852
|
contentSecurityPolicy,
|
|
13764
13853
|
constraintHelpers,
|
|
13765
13854
|
config,
|
|
13855
|
+
compressResponse,
|
|
13766
13856
|
clearNamedRoutes,
|
|
13767
13857
|
buildUrlWithPreservedParams,
|
|
13768
13858
|
benchmarkTests,
|
|
@@ -13773,6 +13863,7 @@ export {
|
|
|
13773
13863
|
authHelpers,
|
|
13774
13864
|
auth,
|
|
13775
13865
|
applyRequestEnhancements,
|
|
13866
|
+
appendVary,
|
|
13776
13867
|
apiKeyAuth,
|
|
13777
13868
|
WebSocketTester,
|
|
13778
13869
|
Validator,
|
|
@@ -13854,6 +13945,7 @@ export {
|
|
|
13854
13945
|
DefaultServiceProviderManager,
|
|
13855
13946
|
DatabaseSessionStore,
|
|
13856
13947
|
DatabaseRules,
|
|
13948
|
+
DEFAULT_COMPRESSION,
|
|
13857
13949
|
DDoSProtection,
|
|
13858
13950
|
Csrf,
|
|
13859
13951
|
Cors,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compressing what goes back, which nothing was doing.
|
|
3
|
+
*
|
|
4
|
+
* Static files have been served gzipped for a long time; every response a
|
|
5
|
+
* *route* produced went out whole. A server-rendered page is the shape that
|
|
6
|
+
* costs: the landing page of the application this was found in is 253 KB of
|
|
7
|
+
* HTML and compresses to about a tenth of that, on every request, for every
|
|
8
|
+
* visitor - and the same is true of any JSON list long enough to matter.
|
|
9
|
+
*
|
|
10
|
+
* Three rules:
|
|
11
|
+
*
|
|
12
|
+
* - **Only when the client asked.** `Accept-Encoding` decides, and `Vary` says
|
|
13
|
+
* so, because a cache that hands a gzipped body to a client that did not ask
|
|
14
|
+
* is a cache that breaks that client.
|
|
15
|
+
* - **Only when it is worth it.** Below a kilobyte the header costs more than
|
|
16
|
+
* the saving, and already-compressed bytes (an image, a woff2, a zip) get
|
|
17
|
+
* bigger rather than smaller.
|
|
18
|
+
* - **Never by buffering.** The body is piped through a `CompressionStream`
|
|
19
|
+
* rather than read into memory, so a response that streams keeps streaming:
|
|
20
|
+
* the diff manifest that sends a hundred files as they parse still arrives a
|
|
21
|
+
* file at a time, compressed. Buffering to compress would have turned this
|
|
22
|
+
* product's progressive render into a wait, which is a worse trade than the
|
|
23
|
+
* bytes are worth.
|
|
24
|
+
*
|
|
25
|
+
* A piped body has no length until it ends, so `Content-Length` comes off and
|
|
26
|
+
* the response goes out chunked. That is the ordinary shape for a compressed
|
|
27
|
+
* response and every client handles it; a browser showing a page as it arrives
|
|
28
|
+
* is not waiting for a number.
|
|
29
|
+
*/
|
|
30
|
+
/** zlib's own range, so a level Bun would reject is a type error rather than a throw. */
|
|
31
|
+
export type CompressionLevel = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
32
|
+
export interface CompressionOptions {
|
|
33
|
+
/** Off turns the whole thing off, for a proxy that already compresses. */
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Kept for callers that set it, and unused by the stream path.
|
|
37
|
+
*
|
|
38
|
+
* `CompressionStream` has no level knob - it is the platform's, at its own
|
|
39
|
+
* default - and piping is what keeps a streamed response streaming. A level
|
|
40
|
+
* would mean going back to buffering, which is the trade this deliberately
|
|
41
|
+
* does not make.
|
|
42
|
+
*/
|
|
43
|
+
level?: CompressionLevel;
|
|
44
|
+
/** Bodies below this many bytes are sent as they are. */
|
|
45
|
+
threshold?: number;
|
|
46
|
+
}
|
|
47
|
+
export declare const DEFAULT_COMPRESSION: Required<CompressionOptions>;
|
|
48
|
+
/**
|
|
49
|
+
* Content types worth compressing.
|
|
50
|
+
*
|
|
51
|
+
* A prefix list rather than a regex over everything: `image/svg+xml` is text
|
|
52
|
+
* and compresses well, `image/png` is already compressed and grows. When in
|
|
53
|
+
* doubt the answer is no, because sending something slightly larger is a
|
|
54
|
+
* wasted CPU cycle and a wasted byte, while failing to compress is only a
|
|
55
|
+
* missed saving.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isCompressible(contentType: string | null): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The encoding to use, from what the client offered.
|
|
60
|
+
*
|
|
61
|
+
* gzip before deflate, and `q=0` respected - a client that says
|
|
62
|
+
* `gzip;q=0` means it, and sending gzip anyway is how you get an unreadable
|
|
63
|
+
* page in the one browser that asked.
|
|
64
|
+
*
|
|
65
|
+
* Brotli is not offered even though Bun can produce it: `Bun.gzipSync` is
|
|
66
|
+
* synchronous and brotli is not, and a compression step that awaits inside the
|
|
67
|
+
* response path is a different change from this one.
|
|
68
|
+
*/
|
|
69
|
+
export declare function negotiateEncoding(header: string | null): 'gzip' | 'deflate' | null;
|
|
70
|
+
/**
|
|
71
|
+
* Whether this response should be compressed at all.
|
|
72
|
+
*
|
|
73
|
+
* `Content-Length` is a *hint here rather than a requirement*: Bun does not put
|
|
74
|
+
* one on a `Response` built from a string, so treating its absence as "this is
|
|
75
|
+
* a stream, leave it alone" left every server-rendered page uncompressed - the
|
|
76
|
+
* exact case this exists for. When there is a length, it decides against
|
|
77
|
+
* bodies too small to be worth it; when there is not, the body is piped and
|
|
78
|
+
* the threshold cannot apply.
|
|
79
|
+
*/
|
|
80
|
+
export declare function shouldCompress(response: Response, encoding: string | null, threshold: number): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The response, compressed when that is the right thing to do.
|
|
83
|
+
*
|
|
84
|
+
* Returns the response it was given when it is not, so a caller can apply this
|
|
85
|
+
* unconditionally at the end of the pipeline.
|
|
86
|
+
*/
|
|
87
|
+
export declare function compressResponse(response: Response, request: Request, options?: CompressionOptions): Promise<Response>;
|
|
88
|
+
/** Add to `Vary` without dropping what is already there. */
|
|
89
|
+
export declare function appendVary(headers: Headers, value: string): void;
|