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.
Files changed (217) hide show
  1. package/README.md +11 -6
  2. package/docs/framework/README.md +3 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/api/presets.md +0 -3
  5. package/docs/framework/cordis/acryl-in-a-cordis-app.md +9 -0
  6. package/docs/framework/getting-started/basic.md +5 -4
  7. package/docs/framework/getting-started/desktop.md +41 -0
  8. package/docs/framework/guides/jsx-dom.md +0 -3
  9. package/docs/framework/guides/jsx.md +1 -4
  10. package/docs/framework/guides/middleware.md +1 -1
  11. package/docs/framework/helpers/cookie.md +1 -1
  12. package/docs/framework/helpers/css.md +0 -3
  13. package/docs/framework/helpers/dev.md +0 -3
  14. package/docs/framework/helpers/jwt.md +1 -1
  15. package/docs/framework/helpers/proxy.md +0 -3
  16. package/docs/framework/middleware/builtin/basic-auth.md +6 -6
  17. package/docs/framework/middleware/builtin/cache.md +0 -3
  18. package/docs/framework/middleware/builtin/combine.md +1 -1
  19. package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
  20. package/docs/framework/middleware/builtin/logger.md +1 -1
  21. package/docs/framework/middleware/builtin/mount.md +0 -3
  22. package/docs/framework/upstream.json +3 -3
  23. package/lib/accepts.d.ts +1 -1
  24. package/lib/acryl-base.d.ts +1 -1
  25. package/lib/adapter.d.ts +1 -1
  26. package/lib/{app-CmAYW1Al.d.ts → app-wFwpg-bt.d.ts} +1 -1
  27. package/lib/base-BpQy2Rk7.d.ts +783 -0
  28. package/lib/base-ddiVmiE7.js +721 -0
  29. package/lib/basic-auth.d.ts +1 -1
  30. package/lib/bearer-auth.d.ts +1 -1
  31. package/lib/body-limit.d.ts +1 -1
  32. package/lib/cache.d.ts +43 -0
  33. package/lib/cache.js +169 -0
  34. package/lib/client.d.ts +2 -2
  35. package/lib/combine.d.ts +1 -1
  36. package/lib/common-D8Z4JRCw.d.ts +46 -0
  37. package/lib/components-Ce5FRyHU.js +268 -0
  38. package/lib/components-DYZdxJIJ.js +24 -0
  39. package/lib/compress.d.ts +1 -1
  40. package/lib/conninfo.d.ts +2 -2
  41. package/lib/constants-BR9jM9BC.js +9 -0
  42. package/lib/{context-Bm7rI-oK.d.ts → context-DVQBjuns.d.ts} +1 -1
  43. package/lib/context-DxFdY_un.d.ts +250 -0
  44. package/lib/context-storage.d.ts +1 -1
  45. package/lib/{cookie-ClMaKDQf.d.ts → cookie-CCelEqky.d.ts} +1 -1
  46. package/lib/cookie.d.ts +3 -3
  47. package/lib/cors.d.ts +1 -1
  48. package/lib/csrf.d.ts +1 -1
  49. package/lib/css-D2jb7X8b.js +320 -0
  50. package/lib/css.d.ts +82 -0
  51. package/lib/css.js +134 -0
  52. package/lib/dev.d.ts +19 -0
  53. package/lib/dev.js +50 -0
  54. package/lib/etag.d.ts +1 -1
  55. package/lib/factory.d.ts +2 -2
  56. package/lib/hooks-Bq5q9IMl.js +762 -0
  57. package/lib/html-SllAe0YT.d.ts +46 -0
  58. package/lib/html.d.ts +1 -1
  59. package/lib/index-C850MeBA.d.ts +94 -0
  60. package/lib/{index-oOClm_Xk.d.ts → index-CUTfU69R.d.ts} +3 -3
  61. package/lib/{index-B-3F4wt2.d.ts → index-Cqcdluyn.d.ts} +2 -2
  62. package/lib/{index-UTBmM3GX.d.ts → index-CsvB-F9j.d.ts} +2 -2
  63. package/lib/index.d.ts +4 -4
  64. package/lib/ip-restriction.d.ts +2 -2
  65. package/lib/jsx/dom/client.d.ts +30 -0
  66. package/lib/jsx/dom/client.js +49 -0
  67. package/lib/jsx/dom/css.d.ts +84 -0
  68. package/lib/jsx/dom/css.js +2 -0
  69. package/lib/jsx/dom/jsx-dev-runtime.d.ts +3 -0
  70. package/lib/jsx/dom/jsx-dev-runtime.js +2 -0
  71. package/lib/jsx/dom/jsx-runtime.d.ts +3 -0
  72. package/lib/jsx/dom/jsx-runtime.js +2 -0
  73. package/lib/jsx/dom/server.d.ts +105 -0
  74. package/lib/jsx/dom/server.js +37 -0
  75. package/lib/jsx/dom.d.ts +5 -0
  76. package/lib/jsx/dom.js +74 -0
  77. package/lib/jsx/jsx-dev-runtime.d.ts +6 -0
  78. package/lib/jsx/jsx-dev-runtime.js +14 -0
  79. package/lib/jsx/jsx-runtime.d.ts +10 -0
  80. package/lib/jsx/jsx-runtime.js +30 -0
  81. package/lib/jsx/streaming.d.ts +2 -0
  82. package/lib/jsx/streaming.js +2 -0
  83. package/lib/jsx-dev-runtime-B22O-Npk.d.ts +7 -0
  84. package/lib/jsx-dev-runtime-kAhR_4C7.js +354 -0
  85. package/lib/jsx-runtime-BVVgDSdq.d.ts +1 -0
  86. package/lib/jsx.d.ts +3 -0
  87. package/lib/jsx.js +47 -0
  88. package/lib/jwk.d.ts +4 -4
  89. package/lib/{jwt-Dg493Tai.d.ts → jwt-BARhotDz.d.ts} +1 -1
  90. package/lib/jwt.d.ts +4 -4
  91. package/lib/language.d.ts +1 -1
  92. package/lib/linear-router-C3FPib6h.js +96 -0
  93. package/lib/logger.d.ts +1 -1
  94. package/lib/loopback-only.d.ts +1 -1
  95. package/lib/method-not-allowed.d.ts +2 -2
  96. package/lib/method-override.d.ts +2 -2
  97. package/lib/mount.d.ts +44 -0
  98. package/lib/mount.js +67 -0
  99. package/lib/node-server/conninfo.d.ts +1 -1
  100. package/lib/node-server/serve-static.d.ts +1 -1
  101. package/lib/node-server/serve-static.js +1 -1
  102. package/lib/node-server.d.ts +1 -1
  103. package/lib/node-server.js +1 -1
  104. package/lib/pattern-router-HZxjDGEQ.js +41 -0
  105. package/lib/powered-by.d.ts +1 -1
  106. package/lib/pretty-json.d.ts +1 -1
  107. package/lib/proxy.d.ts +72 -0
  108. package/lib/proxy.js +121 -0
  109. package/lib/quick.d.ts +8 -0
  110. package/lib/quick.js +17 -0
  111. package/lib/request-id.d.ts +1 -1
  112. package/lib/request.d.ts +1 -1
  113. package/lib/route.d.ts +1 -1
  114. package/lib/router/linear-router.js +1 -95
  115. package/lib/router/pattern-router.js +1 -40
  116. package/lib/schema-validator.d.ts +1 -1
  117. package/lib/secure-headers.d.ts +1 -1
  118. package/lib/{serve-static-Dw_m6KxR.js → serve-static-Cw23shXv.js} +1 -0
  119. package/lib/serve-static.d.ts +1 -1
  120. package/lib/streaming.d.ts +1 -1
  121. package/lib/testing.d.ts +4 -4
  122. package/lib/timeout.d.ts +1 -1
  123. package/lib/timing.d.ts +1 -1
  124. package/lib/tiny.d.ts +8 -0
  125. package/lib/tiny.js +15 -0
  126. package/lib/trailing-slash.d.ts +1 -1
  127. package/lib/{types-CHesD1KG.d.ts → types-CQiGmsWW.d.ts} +3 -3
  128. package/lib/{types-CwrQjlaM.d.ts → types-Lqx5BxQF.d.ts} +10 -1
  129. package/lib/{types-CDO6I3B0.d.ts → types-nEL8VPDC.d.ts} +1 -1
  130. package/lib/types.d.ts +1 -1
  131. package/lib/utils/body.d.ts +1 -1
  132. package/lib/utils/cookie.d.ts +1 -1
  133. package/lib/utils/crypto.d.ts +1 -1
  134. package/lib/utils/html.d.ts +1 -45
  135. package/lib/utils/ipaddr.d.ts +1 -1
  136. package/lib/utils/jwt.d.ts +1 -1
  137. package/lib/utils/types.d.ts +2 -2
  138. package/lib/validator.d.ts +2 -2
  139. package/lib/ws.d.ts +1 -1
  140. package/package.json +69 -1
  141. package/plugins/acryl-inspect/upstream.json +1 -1
  142. package/plugins/acryl-node-server/upstream.json +1 -1
  143. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +506 -0
  144. package/plugins/acryl-router/src/core/helper/css/common.ts +337 -0
  145. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +678 -0
  146. package/plugins/acryl-router/src/core/helper/css/index.ts +262 -0
  147. package/plugins/acryl-router/src/core/helper/dev/index.test.ts +181 -0
  148. package/plugins/acryl-router/src/core/helper/dev/index.ts +79 -0
  149. package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +356 -0
  150. package/plugins/acryl-router/src/core/helper/proxy/index.ts +203 -0
  151. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +557 -0
  152. package/plugins/acryl-router/src/core/jsx/base.test.tsx +97 -0
  153. package/plugins/acryl-router/src/core/jsx/base.ts +490 -0
  154. package/plugins/acryl-router/src/core/jsx/children.test.ts +52 -0
  155. package/plugins/acryl-router/src/core/jsx/children.ts +20 -0
  156. package/plugins/acryl-router/src/core/jsx/components.test.tsx +936 -0
  157. package/plugins/acryl-router/src/core/jsx/components.ts +257 -0
  158. package/plugins/acryl-router/src/core/jsx/constants.ts +6 -0
  159. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +655 -0
  160. package/plugins/acryl-router/src/core/jsx/context.ts +266 -0
  161. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +128 -0
  162. package/plugins/acryl-router/src/core/jsx/dom/client.ts +89 -0
  163. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +330 -0
  164. package/plugins/acryl-router/src/core/jsx/dom/components.ts +39 -0
  165. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +184 -0
  166. package/plugins/acryl-router/src/core/jsx/dom/context.ts +52 -0
  167. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +149 -0
  168. package/plugins/acryl-router/src/core/jsx/dom/css.ts +263 -0
  169. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +172 -0
  170. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.ts +91 -0
  171. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +3681 -0
  172. package/plugins/acryl-router/src/core/jsx/dom/index.ts +170 -0
  173. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +1344 -0
  174. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.ts +438 -0
  175. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.ts +23 -0
  176. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.ts +8 -0
  177. package/plugins/acryl-router/src/core/jsx/dom/render.ts +879 -0
  178. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +199 -0
  179. package/plugins/acryl-router/src/core/jsx/dom/server.ts +76 -0
  180. package/plugins/acryl-router/src/core/jsx/dom/utils.ts +7 -0
  181. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +859 -0
  182. package/plugins/acryl-router/src/core/jsx/hooks/index.ts +437 -0
  183. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +40 -0
  184. package/plugins/acryl-router/src/core/jsx/index.test.tsx +1744 -0
  185. package/plugins/acryl-router/src/core/jsx/index.ts +115 -0
  186. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.ts +23 -0
  187. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +666 -0
  188. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.ts +210 -0
  189. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.ts +938 -0
  190. package/plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts +26 -0
  191. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +85 -0
  192. package/plugins/acryl-router/src/core/jsx/jsx-runtime.ts +51 -0
  193. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +1281 -0
  194. package/plugins/acryl-router/src/core/jsx/streaming.ts +231 -0
  195. package/plugins/acryl-router/src/core/jsx/types.ts +41 -0
  196. package/plugins/acryl-router/src/core/jsx/utils.test.ts +366 -0
  197. package/plugins/acryl-router/src/core/jsx/utils.ts +202 -0
  198. package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +1211 -0
  199. package/plugins/acryl-router/src/core/middleware/cache/index.ts +323 -0
  200. package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +334 -0
  201. package/plugins/acryl-router/src/core/middleware/mount/index.ts +108 -0
  202. package/plugins/acryl-router/src/core/preset/quick.test.ts +42 -0
  203. package/plugins/acryl-router/src/core/preset/quick.ts +24 -0
  204. package/plugins/acryl-router/src/core/preset/tiny.test.ts +42 -0
  205. package/plugins/acryl-router/src/core/preset/tiny.ts +20 -0
  206. package/plugins/acryl-router/src/core/utils/types.ts +9 -0
  207. package/plugins/acryl-router/src/helpers/index.ts +3 -0
  208. package/plugins/acryl-router/src/middleware/index.ts +2 -0
  209. package/plugins/acryl-router/src/service.ts +22 -5
  210. package/plugins/acryl-router/tests/jsx.spec.tsx +63 -0
  211. package/plugins/acryl-router/upstream.json +3 -3
  212. package/plugins/acryl-router-http/upstream.json +1 -1
  213. package/plugins/acryl-state/upstream.json +1 -1
  214. package/plugins/desktop/upstream.json +1 -1
  215. package/plugins/desktop-web/upstream.json +1 -1
  216. package/plugins/save/plugin.json +1 -1
  217. 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
+ })