@stacksjs/bun-router 0.0.23 → 0.0.24
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 +99 -1
- package/dist/response/compression.d.ts +66 -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,97 @@ 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
|
+
const length = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
2103
|
+
if (!Number.isFinite(length))
|
|
2104
|
+
return false;
|
|
2105
|
+
return length >= threshold;
|
|
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 body = new Uint8Array(await response.arrayBuffer());
|
|
2118
|
+
const compressed = encoding === "gzip" ? Bun.gzipSync(body, { level: settings.level }) : Bun.deflateSync(body, { level: settings.level });
|
|
2119
|
+
if (compressed.byteLength >= body.byteLength) {
|
|
2120
|
+
const headers2 = new Headers(response.headers);
|
|
2121
|
+
appendVary(headers2, "Accept-Encoding");
|
|
2122
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: headers2 });
|
|
2123
|
+
}
|
|
2124
|
+
const headers = new Headers(response.headers);
|
|
2125
|
+
headers.set("Content-Encoding", encoding);
|
|
2126
|
+
headers.set("Content-Length", String(compressed.byteLength));
|
|
2127
|
+
appendVary(headers, "Accept-Encoding");
|
|
2128
|
+
return new Response(compressed, { status: response.status, statusText: response.statusText, headers });
|
|
2129
|
+
}
|
|
2130
|
+
function appendVary(headers, value) {
|
|
2131
|
+
const existing = headers.get("vary");
|
|
2132
|
+
if (!existing) {
|
|
2133
|
+
headers.set("Vary", value);
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const parts = existing.split(",").map((part) => part.trim().toLowerCase());
|
|
2137
|
+
if (parts.includes("*") || parts.includes(value.toLowerCase()))
|
|
2138
|
+
return;
|
|
2139
|
+
headers.set("Vary", `${existing}, ${value}`);
|
|
2140
|
+
}
|
|
2050
2141
|
// src/response/response-factory.ts
|
|
2051
2142
|
var response = {
|
|
2052
2143
|
json: (data, options = {}) => {
|
|
@@ -9563,7 +9654,8 @@ function registerServerHandling(RouterClass) {
|
|
|
9563
9654
|
handleRequest: {
|
|
9564
9655
|
async value(req) {
|
|
9565
9656
|
const self = this;
|
|
9566
|
-
|
|
9657
|
+
const response2 = await runWithRequest(req, () => self.handleRequestImpl(req));
|
|
9658
|
+
return await compressResponse(response2, req, self.config?.compression ?? {});
|
|
9567
9659
|
},
|
|
9568
9660
|
writable: true,
|
|
9569
9661
|
configurable: true
|
|
@@ -13664,6 +13756,7 @@ export {
|
|
|
13664
13756
|
spyOnMiddleware,
|
|
13665
13757
|
singleFileUpload,
|
|
13666
13758
|
signRequest,
|
|
13759
|
+
shouldCompress,
|
|
13667
13760
|
setCurrentRequest,
|
|
13668
13761
|
setContainer,
|
|
13669
13762
|
session,
|
|
@@ -13695,6 +13788,7 @@ export {
|
|
|
13695
13788
|
parseThrottleString,
|
|
13696
13789
|
oauth2Auth,
|
|
13697
13790
|
normalizePath,
|
|
13791
|
+
negotiateEncoding,
|
|
13698
13792
|
multipleFileUpload,
|
|
13699
13793
|
modelMocks,
|
|
13700
13794
|
mockMiddleware,
|
|
@@ -13706,6 +13800,7 @@ export {
|
|
|
13706
13800
|
jsonBody,
|
|
13707
13801
|
joinPaths,
|
|
13708
13802
|
isRouteHandler,
|
|
13803
|
+
isCompressible,
|
|
13709
13804
|
isActionClass,
|
|
13710
13805
|
inputValidation,
|
|
13711
13806
|
injectQueryPreservationScript,
|
|
@@ -13763,6 +13858,7 @@ export {
|
|
|
13763
13858
|
contentSecurityPolicy,
|
|
13764
13859
|
constraintHelpers,
|
|
13765
13860
|
config,
|
|
13861
|
+
compressResponse,
|
|
13766
13862
|
clearNamedRoutes,
|
|
13767
13863
|
buildUrlWithPreservedParams,
|
|
13768
13864
|
benchmarkTests,
|
|
@@ -13773,6 +13869,7 @@ export {
|
|
|
13773
13869
|
authHelpers,
|
|
13774
13870
|
auth,
|
|
13775
13871
|
applyRequestEnhancements,
|
|
13872
|
+
appendVary,
|
|
13776
13873
|
apiKeyAuth,
|
|
13777
13874
|
WebSocketTester,
|
|
13778
13875
|
Validator,
|
|
@@ -13854,6 +13951,7 @@ export {
|
|
|
13854
13951
|
DefaultServiceProviderManager,
|
|
13855
13952
|
DatabaseSessionStore,
|
|
13856
13953
|
DatabaseRules,
|
|
13954
|
+
DEFAULT_COMPRESSION,
|
|
13857
13955
|
DDoSProtection,
|
|
13858
13956
|
Csrf,
|
|
13859
13957
|
Cors,
|
|
@@ -0,0 +1,66 @@
|
|
|
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, and the middle one is the important one:
|
|
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 the length is known.** A response with no `Content-Length` is a
|
|
16
|
+
* stream, and buffering one to compress it would turn a progressive render
|
|
17
|
+
* into a wait - the diff manifest that streams a hundred files as they parse
|
|
18
|
+
* would arrive all at once, at the end. Streams are left alone.
|
|
19
|
+
* - **Only when it is worth it.** Below a kilobyte the header costs more than
|
|
20
|
+
* the saving, and already-compressed bytes (an image, a woff2, a zip) get
|
|
21
|
+
* bigger rather than smaller.
|
|
22
|
+
*/
|
|
23
|
+
/** zlib's own range, so a level Bun would reject is a type error rather than a throw. */
|
|
24
|
+
export type CompressionLevel = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
25
|
+
export interface CompressionOptions {
|
|
26
|
+
/** Off turns the whole thing off, for a proxy that already compresses. */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** zlib level. 6 is the usual balance; 9 costs noticeably more CPU for a few percent. */
|
|
29
|
+
level?: CompressionLevel;
|
|
30
|
+
/** Bodies below this many bytes are sent as they are. */
|
|
31
|
+
threshold?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare const DEFAULT_COMPRESSION: Required<CompressionOptions>;
|
|
34
|
+
/**
|
|
35
|
+
* Content types worth compressing.
|
|
36
|
+
*
|
|
37
|
+
* A prefix list rather than a regex over everything: `image/svg+xml` is text
|
|
38
|
+
* and compresses well, `image/png` is already compressed and grows. When in
|
|
39
|
+
* doubt the answer is no, because sending something slightly larger is a
|
|
40
|
+
* wasted CPU cycle and a wasted byte, while failing to compress is only a
|
|
41
|
+
* missed saving.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isCompressible(contentType: string | null): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The encoding to use, from what the client offered.
|
|
46
|
+
*
|
|
47
|
+
* gzip before deflate, and `q=0` respected - a client that says
|
|
48
|
+
* `gzip;q=0` means it, and sending gzip anyway is how you get an unreadable
|
|
49
|
+
* page in the one browser that asked.
|
|
50
|
+
*
|
|
51
|
+
* Brotli is not offered even though Bun can produce it: `Bun.gzipSync` is
|
|
52
|
+
* synchronous and brotli is not, and a compression step that awaits inside the
|
|
53
|
+
* response path is a different change from this one.
|
|
54
|
+
*/
|
|
55
|
+
export declare function negotiateEncoding(header: string | null): 'gzip' | 'deflate' | null;
|
|
56
|
+
/** Whether this response should be compressed at all, and why not when it should not. */
|
|
57
|
+
export declare function shouldCompress(response: Response, encoding: string | null, threshold: number): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The response, compressed when that is the right thing to do.
|
|
60
|
+
*
|
|
61
|
+
* Returns the response it was given when it is not, so a caller can apply this
|
|
62
|
+
* unconditionally at the end of the pipeline.
|
|
63
|
+
*/
|
|
64
|
+
export declare function compressResponse(response: Response, request: Request, options?: CompressionOptions): Promise<Response>;
|
|
65
|
+
/** Add to `Vary` without dropping what is already there. */
|
|
66
|
+
export declare function appendVary(headers: Headers, value: string): void;
|