acrylblends 0.6.1 → 0.6.3
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/README.md +11 -6
- package/docs/framework/README.md +3 -2
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/api/presets.md +0 -3
- package/docs/framework/cordis/acryl-in-a-cordis-app.md +9 -0
- package/docs/framework/getting-started/basic.md +5 -4
- package/docs/framework/getting-started/desktop.md +41 -0
- package/docs/framework/guides/jsx-dom.md +0 -3
- package/docs/framework/guides/jsx.md +1 -4
- package/docs/framework/guides/middleware.md +1 -1
- package/docs/framework/helpers/cookie.md +1 -1
- package/docs/framework/helpers/css.md +0 -3
- package/docs/framework/helpers/dev.md +0 -3
- package/docs/framework/helpers/jwt.md +1 -1
- package/docs/framework/helpers/proxy.md +0 -3
- package/docs/framework/middleware/builtin/basic-auth.md +6 -6
- package/docs/framework/middleware/builtin/cache.md +0 -3
- package/docs/framework/middleware/builtin/combine.md +1 -1
- package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
- package/docs/framework/middleware/builtin/logger.md +1 -1
- package/docs/framework/middleware/builtin/mount.md +0 -3
- package/docs/framework/upstream.json +3 -3
- package/lib/accepts.d.ts +1 -1
- package/lib/acryl-base.d.ts +1 -1
- package/lib/adapter.d.ts +1 -1
- package/lib/{app-CmAYW1Al.d.ts → app-wFwpg-bt.d.ts} +1 -1
- package/lib/base-BpQy2Rk7.d.ts +783 -0
- package/lib/base-ddiVmiE7.js +721 -0
- package/lib/basic-auth.d.ts +1 -1
- package/lib/bearer-auth.d.ts +1 -1
- package/lib/body-limit.d.ts +1 -1
- package/lib/cache.d.ts +43 -0
- package/lib/cache.js +169 -0
- package/lib/client.d.ts +2 -2
- package/lib/combine.d.ts +1 -1
- package/lib/common-D8Z4JRCw.d.ts +46 -0
- package/lib/components-Ce5FRyHU.js +268 -0
- package/lib/components-DYZdxJIJ.js +24 -0
- package/lib/compress.d.ts +1 -1
- package/lib/conninfo.d.ts +2 -2
- package/lib/constants-BR9jM9BC.js +9 -0
- package/lib/{context-Bm7rI-oK.d.ts → context-DVQBjuns.d.ts} +1 -1
- package/lib/context-DxFdY_un.d.ts +250 -0
- package/lib/context-storage.d.ts +1 -1
- package/lib/{cookie-ClMaKDQf.d.ts → cookie-CCelEqky.d.ts} +1 -1
- package/lib/cookie.d.ts +3 -3
- package/lib/cors.d.ts +1 -1
- package/lib/csrf.d.ts +1 -1
- package/lib/css-D2jb7X8b.js +320 -0
- package/lib/css.d.ts +82 -0
- package/lib/css.js +134 -0
- package/lib/dev.d.ts +19 -0
- package/lib/dev.js +50 -0
- package/lib/etag.d.ts +1 -1
- package/lib/factory.d.ts +2 -2
- package/lib/hooks-Bq5q9IMl.js +762 -0
- package/lib/html-SllAe0YT.d.ts +46 -0
- package/lib/html.d.ts +1 -1
- package/lib/index-C850MeBA.d.ts +94 -0
- package/lib/{index-oOClm_Xk.d.ts → index-CUTfU69R.d.ts} +3 -3
- package/lib/{index-B-3F4wt2.d.ts → index-Cqcdluyn.d.ts} +2 -2
- package/lib/{index-UTBmM3GX.d.ts → index-CsvB-F9j.d.ts} +2 -2
- package/lib/index.d.ts +4 -4
- package/lib/ip-restriction.d.ts +2 -2
- package/lib/jsx/dom/client.d.ts +30 -0
- package/lib/jsx/dom/client.js +49 -0
- package/lib/jsx/dom/css.d.ts +84 -0
- package/lib/jsx/dom/css.js +2 -0
- package/lib/jsx/dom/jsx-dev-runtime.d.ts +3 -0
- package/lib/jsx/dom/jsx-dev-runtime.js +2 -0
- package/lib/jsx/dom/jsx-runtime.d.ts +3 -0
- package/lib/jsx/dom/jsx-runtime.js +2 -0
- package/lib/jsx/dom/server.d.ts +105 -0
- package/lib/jsx/dom/server.js +37 -0
- package/lib/jsx/dom.d.ts +5 -0
- package/lib/jsx/dom.js +74 -0
- package/lib/jsx/jsx-dev-runtime.d.ts +6 -0
- package/lib/jsx/jsx-dev-runtime.js +14 -0
- package/lib/jsx/jsx-runtime.d.ts +10 -0
- package/lib/jsx/jsx-runtime.js +30 -0
- package/lib/jsx/streaming.d.ts +2 -0
- package/lib/jsx/streaming.js +2 -0
- package/lib/jsx-dev-runtime-B22O-Npk.d.ts +7 -0
- package/lib/jsx-dev-runtime-kAhR_4C7.js +354 -0
- package/lib/jsx-runtime-BVVgDSdq.d.ts +1 -0
- package/lib/jsx.d.ts +3 -0
- package/lib/jsx.js +47 -0
- package/lib/jwk.d.ts +4 -4
- package/lib/{jwt-Dg493Tai.d.ts → jwt-BARhotDz.d.ts} +1 -1
- package/lib/jwt.d.ts +4 -4
- package/lib/language.d.ts +1 -1
- package/lib/linear-router-C3FPib6h.js +96 -0
- package/lib/logger.d.ts +1 -1
- package/lib/loopback-only.d.ts +1 -1
- package/lib/method-not-allowed.d.ts +2 -2
- package/lib/method-override.d.ts +2 -2
- package/lib/mount.d.ts +44 -0
- package/lib/mount.js +67 -0
- package/lib/node-server/conninfo.d.ts +1 -1
- package/lib/node-server/serve-static.d.ts +1 -1
- package/lib/node-server/serve-static.js +1 -1
- package/lib/node-server.d.ts +1 -1
- package/lib/node-server.js +1 -1
- package/lib/pattern-router-HZxjDGEQ.js +41 -0
- package/lib/powered-by.d.ts +1 -1
- package/lib/pretty-json.d.ts +1 -1
- package/lib/proxy.d.ts +72 -0
- package/lib/proxy.js +121 -0
- package/lib/quick.d.ts +8 -0
- package/lib/quick.js +17 -0
- package/lib/request-id.d.ts +1 -1
- package/lib/request.d.ts +1 -1
- package/lib/route.d.ts +1 -1
- package/lib/router/linear-router.js +1 -95
- package/lib/router/pattern-router.js +1 -40
- package/lib/schema-validator.d.ts +1 -1
- package/lib/secure-headers.d.ts +1 -1
- package/lib/{serve-static-Dw_m6KxR.js → serve-static-Cw23shXv.js} +1 -0
- package/lib/serve-static.d.ts +1 -1
- package/lib/streaming.d.ts +1 -1
- package/lib/testing.d.ts +4 -4
- package/lib/timeout.d.ts +1 -1
- package/lib/timing.d.ts +1 -1
- package/lib/tiny.d.ts +8 -0
- package/lib/tiny.js +15 -0
- package/lib/trailing-slash.d.ts +1 -1
- package/lib/{types-CHesD1KG.d.ts → types-CQiGmsWW.d.ts} +3 -3
- package/lib/{types-CwrQjlaM.d.ts → types-Lqx5BxQF.d.ts} +10 -1
- package/lib/{types-CDO6I3B0.d.ts → types-nEL8VPDC.d.ts} +1 -1
- package/lib/types.d.ts +1 -1
- package/lib/utils/body.d.ts +1 -1
- package/lib/utils/cookie.d.ts +1 -1
- package/lib/utils/crypto.d.ts +1 -1
- package/lib/utils/html.d.ts +1 -45
- package/lib/utils/ipaddr.d.ts +1 -1
- package/lib/utils/jwt.d.ts +1 -1
- package/lib/utils/types.d.ts +2 -2
- package/lib/validator.d.ts +2 -2
- package/lib/ws.d.ts +1 -1
- package/package.json +69 -1
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-node-server/upstream.json +1 -1
- package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +506 -0
- package/plugins/acryl-router/src/core/helper/css/common.ts +337 -0
- package/plugins/acryl-router/src/core/helper/css/index.test.tsx +678 -0
- package/plugins/acryl-router/src/core/helper/css/index.ts +262 -0
- package/plugins/acryl-router/src/core/helper/dev/index.test.ts +181 -0
- package/plugins/acryl-router/src/core/helper/dev/index.ts +79 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +356 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.ts +203 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +557 -0
- package/plugins/acryl-router/src/core/jsx/base.test.tsx +97 -0
- package/plugins/acryl-router/src/core/jsx/base.ts +490 -0
- package/plugins/acryl-router/src/core/jsx/children.test.ts +52 -0
- package/plugins/acryl-router/src/core/jsx/children.ts +20 -0
- package/plugins/acryl-router/src/core/jsx/components.test.tsx +936 -0
- package/plugins/acryl-router/src/core/jsx/components.ts +257 -0
- package/plugins/acryl-router/src/core/jsx/constants.ts +6 -0
- package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +655 -0
- package/plugins/acryl-router/src/core/jsx/context.ts +266 -0
- package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +128 -0
- package/plugins/acryl-router/src/core/jsx/dom/client.ts +89 -0
- package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +330 -0
- package/plugins/acryl-router/src/core/jsx/dom/components.ts +39 -0
- package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +184 -0
- package/plugins/acryl-router/src/core/jsx/dom/context.ts +52 -0
- package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +149 -0
- package/plugins/acryl-router/src/core/jsx/dom/css.ts +263 -0
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +172 -0
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.ts +91 -0
- package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +3681 -0
- package/plugins/acryl-router/src/core/jsx/dom/index.ts +170 -0
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +1344 -0
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.ts +438 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.ts +23 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.ts +8 -0
- package/plugins/acryl-router/src/core/jsx/dom/render.ts +879 -0
- package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +199 -0
- package/plugins/acryl-router/src/core/jsx/dom/server.ts +76 -0
- package/plugins/acryl-router/src/core/jsx/dom/utils.ts +7 -0
- package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +859 -0
- package/plugins/acryl-router/src/core/jsx/hooks/index.ts +437 -0
- package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +40 -0
- package/plugins/acryl-router/src/core/jsx/index.test.tsx +1744 -0
- package/plugins/acryl-router/src/core/jsx/index.ts +115 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.ts +23 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +666 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.ts +210 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-elements.ts +938 -0
- package/plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts +26 -0
- package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +85 -0
- package/plugins/acryl-router/src/core/jsx/jsx-runtime.ts +51 -0
- package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +1281 -0
- package/plugins/acryl-router/src/core/jsx/streaming.ts +231 -0
- package/plugins/acryl-router/src/core/jsx/types.ts +41 -0
- package/plugins/acryl-router/src/core/jsx/utils.test.ts +366 -0
- package/plugins/acryl-router/src/core/jsx/utils.ts +202 -0
- package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +1211 -0
- package/plugins/acryl-router/src/core/middleware/cache/index.ts +323 -0
- package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +334 -0
- package/plugins/acryl-router/src/core/middleware/mount/index.ts +108 -0
- package/plugins/acryl-router/src/core/preset/quick.test.ts +42 -0
- package/plugins/acryl-router/src/core/preset/quick.ts +24 -0
- package/plugins/acryl-router/src/core/preset/tiny.test.ts +42 -0
- package/plugins/acryl-router/src/core/preset/tiny.ts +20 -0
- package/plugins/acryl-router/src/core/utils/types.ts +9 -0
- package/plugins/acryl-router/src/helpers/index.ts +3 -0
- package/plugins/acryl-router/src/middleware/index.ts +2 -0
- package/plugins/acryl-router/src/service.ts +22 -5
- package/plugins/acryl-router/tests/jsx.spec.tsx +63 -0
- package/plugins/acryl-router/upstream.json +3 -3
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/save/plugin.json +1 -1
- package/templates/todo/upstream.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Cache Middleware for Acryl.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context } from '../../context.ts'
|
|
7
|
+
import { cloneRawRequest } from '../../request.ts'
|
|
8
|
+
import type { MiddlewareHandler } from '../../types.ts'
|
|
9
|
+
import { sha256 } from '../../utils/crypto.ts'
|
|
10
|
+
import type { StatusCode } from '../../utils/http-status.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* status codes that can be cached by default.
|
|
14
|
+
*/
|
|
15
|
+
const defaultCacheableStatusCodes: ReadonlyArray<StatusCode> = [200]
|
|
16
|
+
|
|
17
|
+
const defaultMaxQueryBodySize = 64 * 1024
|
|
18
|
+
|
|
19
|
+
const cacheKeyPath = '/.acryl-router/cache'
|
|
20
|
+
const cacheKeyParameter = '__hono_cache_key'
|
|
21
|
+
const cacheMethodKeyParameter = '__hono_cache_method'
|
|
22
|
+
const queryDigestKeyParameter = '__hono_query_digest'
|
|
23
|
+
const cacheVaryKeyParameter = '__hono_cache_vary'
|
|
24
|
+
|
|
25
|
+
type CacheKeyRequest =
|
|
26
|
+
| { method: 'GET' }
|
|
27
|
+
| {
|
|
28
|
+
method: 'QUERY'
|
|
29
|
+
digest: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const queryRepresentationMetadataHeaders = [
|
|
33
|
+
'content-type',
|
|
34
|
+
'content-encoding',
|
|
35
|
+
'content-language',
|
|
36
|
+
'content-location',
|
|
37
|
+
] as const
|
|
38
|
+
|
|
39
|
+
const shouldSkipCacheControl = (cacheControl: string | null): boolean =>
|
|
40
|
+
!!cacheControl && /(?:^|,\s*)(?:private|no-(?:store|cache))(?:\s*(?:=|,|$))/i.test(cacheControl)
|
|
41
|
+
|
|
42
|
+
const parseVaryDirectives = (vary: string | string[] | null | undefined): string[] => {
|
|
43
|
+
if (vary == null) {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
return (Array.isArray(vary) ? vary : vary.split(','))
|
|
47
|
+
.map((directive) => directive.trim().toLowerCase())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const createCacheKey = (
|
|
52
|
+
key: string,
|
|
53
|
+
requestUrl: string,
|
|
54
|
+
request: CacheKeyRequest,
|
|
55
|
+
varyHeaders: [string, string][]
|
|
56
|
+
): string => {
|
|
57
|
+
const url = new URL(cacheKeyPath, requestUrl)
|
|
58
|
+
url.searchParams.append(cacheKeyParameter, key)
|
|
59
|
+
url.searchParams.append(cacheMethodKeyParameter, request.method)
|
|
60
|
+
|
|
61
|
+
if (request.method === 'QUERY') {
|
|
62
|
+
url.searchParams.append(queryDigestKeyParameter, request.digest)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const header of varyHeaders) {
|
|
66
|
+
url.searchParams.append(cacheVaryKeyParameter, JSON.stringify(header))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return url.href
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const shouldSkipCache = (
|
|
73
|
+
res: Response,
|
|
74
|
+
optionsVaryDirectives: Set<string> | undefined,
|
|
75
|
+
responseVary: string[]
|
|
76
|
+
): boolean =>
|
|
77
|
+
(responseVary.length &&
|
|
78
|
+
(!optionsVaryDirectives || responseVary.some((name) => !optionsVaryDirectives.has(name)))) ||
|
|
79
|
+
shouldSkipCacheControl(res.headers.get('Cache-Control')) ||
|
|
80
|
+
res.headers.has('Set-Cookie')
|
|
81
|
+
|
|
82
|
+
const reportCacheNotAvailable = (
|
|
83
|
+
onCacheNotAvailable: ((reason: string) => void) | false | undefined,
|
|
84
|
+
reason: string
|
|
85
|
+
): void => {
|
|
86
|
+
if (onCacheNotAvailable === false) {
|
|
87
|
+
// suppress log
|
|
88
|
+
} else if (onCacheNotAvailable) {
|
|
89
|
+
onCacheNotAvailable(reason)
|
|
90
|
+
} else {
|
|
91
|
+
console.log(reason)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const createQueryDigest = async (
|
|
96
|
+
c: Context,
|
|
97
|
+
maxQueryBodySize: number
|
|
98
|
+
): Promise<string | undefined> => {
|
|
99
|
+
if (!globalThis.crypto?.subtle) {
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (c.req.raw.bodyUsed && Object.keys(c.req.bodyCache)[0] === 'formData') {
|
|
104
|
+
// FormData cannot be reserialized with a stable multipart boundary after
|
|
105
|
+
// the original request body has been consumed.
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// RFC 10008 Section 2.7 requires QUERY cache keys to incorporate the
|
|
111
|
+
// request content and its related representation metadata.
|
|
112
|
+
const requestHeaders = c.req.raw.headers
|
|
113
|
+
const metadata = new TextEncoder().encode(
|
|
114
|
+
JSON.stringify(
|
|
115
|
+
queryRepresentationMetadataHeaders.map((header) => [header, requestHeaders.get(header)])
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
const body = (await cloneRawRequest(c.req)).body
|
|
119
|
+
const chunks: Uint8Array[] = []
|
|
120
|
+
let bodySize = 0
|
|
121
|
+
|
|
122
|
+
if (body) {
|
|
123
|
+
const reader = body.getReader()
|
|
124
|
+
for (;;) {
|
|
125
|
+
const { done, value } = await reader.read()
|
|
126
|
+
if (done) {
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
bodySize += value.byteLength
|
|
130
|
+
if (bodySize > maxQueryBodySize) {
|
|
131
|
+
// Do not await cancellation because a cloned stream's cancellation
|
|
132
|
+
// can wait for the original request stream to finish.
|
|
133
|
+
void reader.cancel().catch(() => {})
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
chunks.push(value)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data = new Uint8Array(metadata.byteLength + bodySize)
|
|
141
|
+
data.set(metadata)
|
|
142
|
+
let offset = metadata.byteLength
|
|
143
|
+
for (const chunk of chunks) {
|
|
144
|
+
data.set(chunk, offset)
|
|
145
|
+
offset += chunk.byteLength
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (await sha256(data)) ?? undefined
|
|
149
|
+
} catch {
|
|
150
|
+
// A QUERY response cannot be cached safely if its content cannot be read.
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Cache Middleware for Acryl.
|
|
157
|
+
*
|
|
158
|
+
*
|
|
159
|
+
* @param {Object} options - The options for the cache middleware.
|
|
160
|
+
* @param {string | Function} options.cacheName - The name of the cache. Can be used to store multiple caches with different identifiers.
|
|
161
|
+
* @param {boolean} [options.wait=false] - A boolean indicating if Acryl should wait for the Promise of the `cache.put` function to resolve before continuing with the request. Required to be true for the Deno environment.
|
|
162
|
+
* @param {string} [options.cacheControl] - A string of directives for the `Cache-Control` header.
|
|
163
|
+
* @param {string | string[]} [options.vary] - Adds the configured request headers to the cache key variants and sets the `Vary` header in the response. If the original response header already contains a `Vary` header, the values are merged, removing any duplicates.
|
|
164
|
+
* @param {Function} [options.keyGenerator] - Generates keys for every request in the `cacheName` store. This can be used to cache data based on request parameters or context parameters. QUERY keys additionally include a digest of the request content and its representation metadata.
|
|
165
|
+
* @param {number} [options.maxQueryBodySize=65536] - The maximum QUERY request body size in bytes that can be cached. Larger QUERY requests bypass the cache.
|
|
166
|
+
* @param {number[]} [options.cacheableStatusCodes=[200]] - An array of status codes that can be cached.
|
|
167
|
+
* @param {Function | false} [options.onCacheNotAvailable] - A callback invoked with the reason when `globalThis.caches` is not available or QUERY caching cannot use Web Crypto. By default, the reason is logged to the console. Set to `false` to suppress the log, or provide a custom function.
|
|
168
|
+
* @returns {MiddlewareHandler} The middleware handler function.
|
|
169
|
+
* @throws {Error} If the `vary` option includes "*".
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* app.use(
|
|
174
|
+
* '*',
|
|
175
|
+
* cache({
|
|
176
|
+
* cacheName: 'my-app',
|
|
177
|
+
* cacheControl: 'max-age=3600',
|
|
178
|
+
* })
|
|
179
|
+
* )
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export const cache = (options: {
|
|
183
|
+
cacheName: string | ((c: Context) => Promise<string> | string)
|
|
184
|
+
wait?: boolean
|
|
185
|
+
cacheControl?: string
|
|
186
|
+
vary?: string | string[]
|
|
187
|
+
keyGenerator?: (c: Context) => Promise<string> | string
|
|
188
|
+
maxQueryBodySize?: number
|
|
189
|
+
cacheableStatusCodes?: StatusCode[]
|
|
190
|
+
onCacheNotAvailable?: ((reason: string) => void) | false
|
|
191
|
+
}): MiddlewareHandler => {
|
|
192
|
+
if (!globalThis.caches) {
|
|
193
|
+
reportCacheNotAvailable(
|
|
194
|
+
options.onCacheNotAvailable,
|
|
195
|
+
'Cache Middleware is not enabled because caches is not defined.'
|
|
196
|
+
)
|
|
197
|
+
return async (_c, next) => await next()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!globalThis.crypto?.subtle) {
|
|
201
|
+
reportCacheNotAvailable(
|
|
202
|
+
options.onCacheNotAvailable,
|
|
203
|
+
'Cache Middleware cannot cache QUERY requests because Web Crypto is not available.'
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (options.wait === undefined) {
|
|
208
|
+
options.wait = false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const cacheControlDirectives = options.cacheControl
|
|
212
|
+
?.split(',')
|
|
213
|
+
.map((directive) => directive.toLowerCase())
|
|
214
|
+
const optionsVaryList = parseVaryDirectives(options.vary)
|
|
215
|
+
const varyDirectives = optionsVaryList.length ? new Set(optionsVaryList) : undefined
|
|
216
|
+
// RFC 7231 Section 7.1.4 specifies that "*" is not allowed in Vary header.
|
|
217
|
+
// See: https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.4
|
|
218
|
+
if (varyDirectives?.has('*')) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
'Middleware vary configuration cannot include "*", as it disallows effective caching.'
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const cacheableStatusCodes = new Set<number>(
|
|
225
|
+
options.cacheableStatusCodes ?? defaultCacheableStatusCodes
|
|
226
|
+
)
|
|
227
|
+
const maxQueryBodySize = options.maxQueryBodySize ?? defaultMaxQueryBodySize
|
|
228
|
+
|
|
229
|
+
const addHeader = (c: Context, responseVary: string[]) => {
|
|
230
|
+
if (cacheControlDirectives) {
|
|
231
|
+
const existingDirectives =
|
|
232
|
+
c.res.headers
|
|
233
|
+
.get('Cache-Control')
|
|
234
|
+
?.split(',')
|
|
235
|
+
// Directive names are case-insensitive (RFC 7234 §5.2); lower-case so
|
|
236
|
+
// the case-insensitive de-dup check below matches handler-set names
|
|
237
|
+
// like `Max-Age`.
|
|
238
|
+
.map((d) => d.trim().split('=', 1)[0].toLowerCase()) ?? []
|
|
239
|
+
for (const directive of cacheControlDirectives) {
|
|
240
|
+
let [name, value] = directive.trim().split('=', 2)
|
|
241
|
+
name = name.toLowerCase()
|
|
242
|
+
if (!existingDirectives.includes(name)) {
|
|
243
|
+
c.header('Cache-Control', `${name}${value ? `=${value}` : ''}`, { append: true })
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (varyDirectives) {
|
|
249
|
+
if (responseVary.length === 0) {
|
|
250
|
+
c.header('Vary', Array.from(varyDirectives).join(', '))
|
|
251
|
+
} else {
|
|
252
|
+
const merged = new Set(varyDirectives)
|
|
253
|
+
for (const directive of responseVary) {
|
|
254
|
+
merged.add(directive)
|
|
255
|
+
}
|
|
256
|
+
if (merged.has('*')) {
|
|
257
|
+
c.header('Vary', '*')
|
|
258
|
+
} else {
|
|
259
|
+
c.header('Vary', Array.from(merged).join(', '))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return async function cache(c, next) {
|
|
266
|
+
if (
|
|
267
|
+
(c.req.method !== 'GET' && c.req.method !== 'QUERY') ||
|
|
268
|
+
c.req.raw.headers.has('Authorization')
|
|
269
|
+
) {
|
|
270
|
+
await next()
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let cacheKeyRequest: CacheKeyRequest = { method: 'GET' }
|
|
275
|
+
if (c.req.method === 'QUERY') {
|
|
276
|
+
const digest = await createQueryDigest(c, maxQueryBodySize)
|
|
277
|
+
if (digest === undefined) {
|
|
278
|
+
await next()
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
cacheKeyRequest = { method: 'QUERY', digest }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let key = c.req.url
|
|
285
|
+
if (options.keyGenerator) {
|
|
286
|
+
key = await options.keyGenerator(c)
|
|
287
|
+
}
|
|
288
|
+
const varyHeaders: [string, string][] = []
|
|
289
|
+
if (varyDirectives) {
|
|
290
|
+
for (const directive of varyDirectives) {
|
|
291
|
+
const value = c.req.raw.headers.get(directive) ?? ''
|
|
292
|
+
varyHeaders.push([directive, value])
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
key = createCacheKey(key, c.req.url, cacheKeyRequest, varyHeaders)
|
|
296
|
+
|
|
297
|
+
const cacheName =
|
|
298
|
+
typeof options.cacheName === 'function' ? await options.cacheName(c) : options.cacheName
|
|
299
|
+
const cache = await caches.open(cacheName)
|
|
300
|
+
const response = await cache.match(key)
|
|
301
|
+
if (response) {
|
|
302
|
+
return new Response(response.body, response)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await next()
|
|
306
|
+
if (!cacheableStatusCodes.has(c.res.status)) {
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
const responseVary = parseVaryDirectives(c.res.headers.get('Vary'))
|
|
310
|
+
addHeader(c, responseVary)
|
|
311
|
+
|
|
312
|
+
if (shouldSkipCache(c.res, varyDirectives, responseVary)) {
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const res = c.res.clone()
|
|
317
|
+
if (options.wait) {
|
|
318
|
+
await cache.put(key, res)
|
|
319
|
+
} else {
|
|
320
|
+
c.executionCtx.waitUntil(cache.put(key, res))
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { mount } from './index.ts'
|
|
2
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
3
|
+
import type { ExecutionContext } from '../../context.ts'
|
|
4
|
+
import { Acryl } from '../../app.ts'
|
|
5
|
+
import { getPath } from '../../utils/url.ts'
|
|
6
|
+
|
|
7
|
+
describe('mount()', () => {
|
|
8
|
+
describe('Basic', () => {
|
|
9
|
+
const anotherApp = (req: Request, ...params: unknown[]) => {
|
|
10
|
+
const path = getPath(req)
|
|
11
|
+
if (path === '/') {
|
|
12
|
+
return new Response('AnotherApp')
|
|
13
|
+
}
|
|
14
|
+
if (path === '/hello') {
|
|
15
|
+
return new Response('Hello from AnotherApp')
|
|
16
|
+
}
|
|
17
|
+
if (path === '/header') {
|
|
18
|
+
const message = req.headers.get('x-message')
|
|
19
|
+
return new Response(message)
|
|
20
|
+
}
|
|
21
|
+
if (path === '/with-query') {
|
|
22
|
+
const queryStrings = new URL(req.url).searchParams.toString()
|
|
23
|
+
return new Response(queryStrings)
|
|
24
|
+
}
|
|
25
|
+
if (path == '/with-params') {
|
|
26
|
+
return new Response(
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
params,
|
|
29
|
+
}),
|
|
30
|
+
{
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application.json',
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
if (path === '/undefined') {
|
|
38
|
+
return undefined as unknown as Response
|
|
39
|
+
}
|
|
40
|
+
return new Response('Not Found from AnotherApp', {
|
|
41
|
+
status: 404,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const app = new Acryl()
|
|
46
|
+
app.use('*', async (c, next) => {
|
|
47
|
+
await next()
|
|
48
|
+
c.header('x-message', 'Foo')
|
|
49
|
+
})
|
|
50
|
+
app.get('/', (c) => c.text('Acryl'))
|
|
51
|
+
app.notFound((c) => {
|
|
52
|
+
return c.text('Not Found from App', 404)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
app.all(
|
|
56
|
+
'/another-app/*',
|
|
57
|
+
mount(anotherApp, () => {
|
|
58
|
+
return 'params'
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
app.all(
|
|
62
|
+
'/another-app-with-array-option/*',
|
|
63
|
+
mount(anotherApp, () => {
|
|
64
|
+
return ['param1', 'param2']
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
app.all('/another-app2/sub-slash/*', mount(anotherApp))
|
|
68
|
+
|
|
69
|
+
const api = new Acryl().basePath('/api')
|
|
70
|
+
api.all('/another-app/*', mount(anotherApp))
|
|
71
|
+
|
|
72
|
+
it('Should return responses from Acryl app', async () => {
|
|
73
|
+
const res = await app.request('/')
|
|
74
|
+
expect(res.status).toBe(200)
|
|
75
|
+
expect(res.headers.get('x-message')).toBe('Foo')
|
|
76
|
+
expect(await res.text()).toBe('Acryl')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('Should return responses from AnotherApp', async () => {
|
|
80
|
+
let res = await app.request('/another-app')
|
|
81
|
+
expect(res.status).toBe(200)
|
|
82
|
+
expect(res.headers.get('x-message')).toBe('Foo')
|
|
83
|
+
expect(await res.text()).toBe('AnotherApp')
|
|
84
|
+
|
|
85
|
+
res = await app.request('/another-app/hello')
|
|
86
|
+
expect(res.status).toBe(200)
|
|
87
|
+
expect(res.headers.get('x-message')).toBe('Foo')
|
|
88
|
+
expect(await res.text()).toBe('Hello from AnotherApp')
|
|
89
|
+
|
|
90
|
+
const req = new Request('http://localhost/another-app/header', {
|
|
91
|
+
headers: {
|
|
92
|
+
'x-message': 'Message Foo!',
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
res = await app.request(req)
|
|
96
|
+
expect(res.status).toBe(200)
|
|
97
|
+
expect(res.headers.get('x-message')).toBe('Foo')
|
|
98
|
+
expect(await res.text()).toBe('Message Foo!')
|
|
99
|
+
|
|
100
|
+
res = await app.request('/another-app/not-found')
|
|
101
|
+
expect(res.status).toBe(404)
|
|
102
|
+
expect(res.headers.get('x-message')).toBe('Foo')
|
|
103
|
+
expect(await res.text()).toBe('Not Found from AnotherApp')
|
|
104
|
+
|
|
105
|
+
res = await app.request('/another-app/with-query?foo=bar&baz=qux')
|
|
106
|
+
expect(res.status).toBe(200)
|
|
107
|
+
expect(await res.text()).toBe('foo=bar&baz=qux')
|
|
108
|
+
|
|
109
|
+
res = await app.request('/another-app/with-params')
|
|
110
|
+
expect(res.status).toBe(200)
|
|
111
|
+
expect(await res.json()).toEqual({
|
|
112
|
+
params: ['params'],
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
res = await app.request('/another-app/undefined')
|
|
116
|
+
expect(res.status).toBe(404)
|
|
117
|
+
expect(await res.text()).toBe('Not Found from App')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('Should return response from Another app with an array option', async () => {
|
|
121
|
+
const res = await app.request('/another-app-with-array-option/with-params')
|
|
122
|
+
expect(res.status).toBe(200)
|
|
123
|
+
expect(await res.json()).toEqual({
|
|
124
|
+
params: ['param1', 'param2'],
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('Should return responses from AnotherApp - sub + slash', async () => {
|
|
129
|
+
const res = await app.request('/another-app2/sub-slash')
|
|
130
|
+
expect(res.status).toBe(200)
|
|
131
|
+
expect(await res.text()).toBe('AnotherApp')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('Should return responses from AnotherApp - with `basePath()`', async () => {
|
|
135
|
+
const res = await api.request('/api/another-app')
|
|
136
|
+
expect(res.status).toBe(200)
|
|
137
|
+
expect(await res.text()).toBe('AnotherApp')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('With encoded paths', () => {
|
|
142
|
+
const anotherApp = (req: Request) => new Response(getPath(req))
|
|
143
|
+
|
|
144
|
+
it('Should strip a decoded non-ASCII mount prefix', async () => {
|
|
145
|
+
const app = new Acryl()
|
|
146
|
+
app.all('/api/é/*', mount(anotherApp))
|
|
147
|
+
|
|
148
|
+
const res = await app.request('/api/%C3%A9/hello')
|
|
149
|
+
|
|
150
|
+
expect(res.status).toBe(200)
|
|
151
|
+
expect(await res.text()).toBe('/hello')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('Should preserve an encoded slash as a literal path segment after stripping the prefix', async () => {
|
|
155
|
+
const app = new Acryl()
|
|
156
|
+
app.all('/api/v1/*', mount(anotherApp))
|
|
157
|
+
|
|
158
|
+
const res = await app.request('/api/v1/admin%2Fsecret')
|
|
159
|
+
|
|
160
|
+
expect(res.status).toBe(200)
|
|
161
|
+
expect(await res.text()).toBe('/admin%2Fsecret')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('Should preserve encoded percent characters after stripping the prefix', async () => {
|
|
165
|
+
const app = new Acryl()
|
|
166
|
+
app.all('/api/*', mount(anotherApp))
|
|
167
|
+
|
|
168
|
+
const res = await app.request('/api/foo%252Fbar')
|
|
169
|
+
|
|
170
|
+
expect(res.status).toBe(200)
|
|
171
|
+
expect(await res.text()).toBe('/foo%252Fbar')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('With fetch', () => {
|
|
176
|
+
const anotherApp = async (req: Request, env: {}, executionContext: ExecutionContext) => {
|
|
177
|
+
const path = getPath(req)
|
|
178
|
+
if (path === '/') {
|
|
179
|
+
return new Response(
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
env,
|
|
182
|
+
executionContext,
|
|
183
|
+
}),
|
|
184
|
+
{
|
|
185
|
+
headers: {
|
|
186
|
+
'Content-Type': 'application/json',
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
return new Response('Not Found from AnotherApp', {
|
|
192
|
+
status: 404,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const app = new Acryl()
|
|
197
|
+
app.all('/another-app/*', mount(anotherApp))
|
|
198
|
+
|
|
199
|
+
it('Should handle Env and ExecuteContext', async () => {
|
|
200
|
+
const request = new Request('http://localhost/another-app')
|
|
201
|
+
const res = await app.fetch(
|
|
202
|
+
request,
|
|
203
|
+
{
|
|
204
|
+
TOKEN: 'foo',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
// Force mocking!
|
|
208
|
+
|
|
209
|
+
// @ts-ignore
|
|
210
|
+
waitUntil: 'waitUntil',
|
|
211
|
+
|
|
212
|
+
// @ts-ignore
|
|
213
|
+
passThroughOnException: 'passThroughOnException',
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
expect(res.status).toBe(200)
|
|
217
|
+
expect(await res.json()).toEqual({
|
|
218
|
+
env: {
|
|
219
|
+
TOKEN: 'foo',
|
|
220
|
+
},
|
|
221
|
+
executionContext: {
|
|
222
|
+
waitUntil: 'waitUntil',
|
|
223
|
+
passThroughOnException: 'passThroughOnException',
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('Mount on `/`', () => {
|
|
230
|
+
const anotherApp = (req: Request, _params: unknown) => {
|
|
231
|
+
const path = getPath(req)
|
|
232
|
+
if (path === '/') {
|
|
233
|
+
return new Response('AnotherApp')
|
|
234
|
+
}
|
|
235
|
+
if (path === '/hello') {
|
|
236
|
+
return new Response('Hello from AnotherApp')
|
|
237
|
+
}
|
|
238
|
+
if (path === '/good/night') {
|
|
239
|
+
return new Response('Good Night from AnotherApp')
|
|
240
|
+
}
|
|
241
|
+
return new Response('Not Found from AnotherApp', {
|
|
242
|
+
status: 404,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const app = new Acryl()
|
|
247
|
+
app.all('/*', mount(anotherApp))
|
|
248
|
+
|
|
249
|
+
it('Should return responses from AnotherApp - mount on `/`', async () => {
|
|
250
|
+
let res = await app.request('/')
|
|
251
|
+
expect(res.status).toBe(200)
|
|
252
|
+
expect(await res.text()).toBe('AnotherApp')
|
|
253
|
+
res = await app.request('/hello')
|
|
254
|
+
expect(res.status).toBe(200)
|
|
255
|
+
expect(await res.text()).toBe('Hello from AnotherApp')
|
|
256
|
+
res = await app.request('/good/night')
|
|
257
|
+
expect(res.status).toBe(200)
|
|
258
|
+
expect(await res.text()).toBe('Good Night from AnotherApp')
|
|
259
|
+
res = await app.request('/not-found')
|
|
260
|
+
expect(res.status).toBe(404)
|
|
261
|
+
expect(await res.text()).toBe('Not Found from AnotherApp')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('With replaceRequest option', () => {
|
|
266
|
+
const anotherApp = (req: Request) => {
|
|
267
|
+
const path = getPath(req)
|
|
268
|
+
if (path === '/app') {
|
|
269
|
+
return new Response(getPath(req))
|
|
270
|
+
}
|
|
271
|
+
return new Response(null, { status: 404 })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const app = new Acryl()
|
|
275
|
+
app.all(
|
|
276
|
+
'/app/*',
|
|
277
|
+
mount(anotherApp, {
|
|
278
|
+
replaceRequest: (req) => req,
|
|
279
|
+
})
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
it('Should return 200 response with the correct path', async () => {
|
|
283
|
+
const res = await app.request('/app')
|
|
284
|
+
expect(res.status).toBe(200)
|
|
285
|
+
expect(await res.text()).toBe('/app')
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe('With replaceRequest: false', () => {
|
|
290
|
+
const anotherApp = (req: Request) => {
|
|
291
|
+
const path = getPath(req)
|
|
292
|
+
if (path === '/app') {
|
|
293
|
+
return new Response(getPath(req))
|
|
294
|
+
}
|
|
295
|
+
return new Response(null, { status: 404 })
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const app = new Acryl()
|
|
299
|
+
app.all('/app/*', mount(anotherApp, { replaceRequest: false }))
|
|
300
|
+
|
|
301
|
+
it('Should return 200 response with the correct path', async () => {
|
|
302
|
+
const res = await app.request('/app')
|
|
303
|
+
expect(res.status).toBe(200)
|
|
304
|
+
expect(await res.text()).toBe('/app')
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe('Used as `app.use()` middleware', () => {
|
|
309
|
+
const anotherApp = (req: Request) => {
|
|
310
|
+
const path = getPath(req)
|
|
311
|
+
if (path === '/hello') {
|
|
312
|
+
return new Response('Hello from AnotherApp')
|
|
313
|
+
}
|
|
314
|
+
// Fall through to the Acryl app
|
|
315
|
+
return undefined as unknown as Response
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const app = new Acryl()
|
|
319
|
+
app.use('/another-app/*', mount(anotherApp))
|
|
320
|
+
app.get('/another-app/fallback', (c) => c.text('Fallback from App'))
|
|
321
|
+
|
|
322
|
+
it('Should return responses from AnotherApp', async () => {
|
|
323
|
+
const res = await app.request('/another-app/hello')
|
|
324
|
+
expect(res.status).toBe(200)
|
|
325
|
+
expect(await res.text()).toBe('Hello from AnotherApp')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('Should fall through to the Acryl app if AnotherApp returns nothing', async () => {
|
|
329
|
+
const res = await app.request('/another-app/fallback')
|
|
330
|
+
expect(res.status).toBe(200)
|
|
331
|
+
expect(await res.text()).toBe('Fallback from App')
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
})
|