@c9up/ream 0.2.28 → 0.2.30

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 (293) hide show
  1. package/README.md +5 -6
  2. package/dist/Codemods.d.ts +24 -0
  3. package/dist/Codemods.d.ts.map +1 -1
  4. package/dist/Codemods.js +211 -1
  5. package/dist/Codemods.js.map +1 -1
  6. package/dist/HttpKernel.d.ts.map +1 -1
  7. package/dist/HttpKernel.js +17 -5
  8. package/dist/HttpKernel.js.map +1 -1
  9. package/dist/Ignitor.d.ts +15 -2
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +33 -23
  12. package/dist/Ignitor.js.map +1 -1
  13. package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
  14. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  15. package/dist/bodyparser/BodyParserMiddleware.js +41 -11
  16. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  17. package/dist/bodyparser/MultipartFile.d.ts +21 -15
  18. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  19. package/dist/bodyparser/MultipartFile.js +40 -58
  20. package/dist/bodyparser/MultipartFile.js.map +1 -1
  21. package/dist/bodyparser/parseSize.d.ts +7 -4
  22. package/dist/bodyparser/parseSize.d.ts.map +1 -1
  23. package/dist/bodyparser/parseSize.js +12 -17
  24. package/dist/bodyparser/parseSize.js.map +1 -1
  25. package/dist/bodyparser/qsParse.d.ts +20 -1
  26. package/dist/bodyparser/qsParse.d.ts.map +1 -1
  27. package/dist/bodyparser/qsParse.js +78 -25
  28. package/dist/bodyparser/qsParse.js.map +1 -1
  29. package/dist/codemods/source.d.ts +57 -0
  30. package/dist/codemods/source.d.ts.map +1 -0
  31. package/dist/codemods/source.js +282 -0
  32. package/dist/codemods/source.js.map +1 -0
  33. package/dist/console/prompts.d.ts.map +1 -1
  34. package/dist/console/prompts.js +3 -1
  35. package/dist/console/prompts.js.map +1 -1
  36. package/dist/container/Container.d.ts.map +1 -1
  37. package/dist/container/Container.js +61 -71
  38. package/dist/container/Container.js.map +1 -1
  39. package/dist/events/Emitter.d.ts +12 -0
  40. package/dist/events/Emitter.d.ts.map +1 -1
  41. package/dist/events/Emitter.js +36 -9
  42. package/dist/events/Emitter.js.map +1 -1
  43. package/dist/events/helix/helpers.d.ts.map +1 -1
  44. package/dist/events/helix/helpers.js +19 -9
  45. package/dist/events/helix/helpers.js.map +1 -1
  46. package/dist/graphql/GraphQLEngine.d.ts +8 -0
  47. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  48. package/dist/graphql/GraphQLEngine.js +79 -12
  49. package/dist/graphql/GraphQLEngine.js.map +1 -1
  50. package/dist/helpers/MessageBuilder.d.ts +4 -2
  51. package/dist/helpers/MessageBuilder.d.ts.map +1 -1
  52. package/dist/helpers/MessageBuilder.js +4 -2
  53. package/dist/helpers/MessageBuilder.js.map +1 -1
  54. package/dist/helpers/errors.d.ts +11 -0
  55. package/dist/helpers/errors.d.ts.map +1 -0
  56. package/dist/helpers/errors.js +17 -0
  57. package/dist/helpers/errors.js.map +1 -0
  58. package/dist/helpers/napi-loader.d.ts.map +1 -1
  59. package/dist/helpers/napi-loader.js +3 -2
  60. package/dist/helpers/napi-loader.js.map +1 -1
  61. package/dist/http/Exception.d.ts +14 -1
  62. package/dist/http/Exception.d.ts.map +1 -1
  63. package/dist/http/Exception.js +10 -2
  64. package/dist/http/Exception.js.map +1 -1
  65. package/dist/http/HttpContext.d.ts +31 -2
  66. package/dist/http/HttpContext.d.ts.map +1 -1
  67. package/dist/http/HttpContext.js +12 -5
  68. package/dist/http/HttpContext.js.map +1 -1
  69. package/dist/http/RedirectBuilder.d.ts +9 -3
  70. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  71. package/dist/http/RedirectBuilder.js +43 -37
  72. package/dist/http/RedirectBuilder.js.map +1 -1
  73. package/dist/http/Request.d.ts +42 -17
  74. package/dist/http/Request.d.ts.map +1 -1
  75. package/dist/http/Request.js +80 -58
  76. package/dist/http/Request.js.map +1 -1
  77. package/dist/http/Response.d.ts +7 -4
  78. package/dist/http/Response.d.ts.map +1 -1
  79. package/dist/http/Response.js +14 -6
  80. package/dist/http/Response.js.map +1 -1
  81. package/dist/http/SseStream.js +6 -7
  82. package/dist/http/SseStream.js.map +1 -1
  83. package/dist/http/permissions.d.ts +25 -0
  84. package/dist/http/permissions.d.ts.map +1 -0
  85. package/dist/http/permissions.js +38 -0
  86. package/dist/http/permissions.js.map +1 -0
  87. package/dist/http-client/HttpClient.d.ts +111 -0
  88. package/dist/http-client/HttpClient.d.ts.map +1 -0
  89. package/dist/http-client/HttpClient.js +386 -0
  90. package/dist/http-client/HttpClient.js.map +1 -0
  91. package/dist/http-client/addresses.d.ts +14 -0
  92. package/dist/http-client/addresses.d.ts.map +1 -0
  93. package/dist/http-client/addresses.js +57 -0
  94. package/dist/http-client/addresses.js.map +1 -0
  95. package/dist/http-client/fake.d.ts +50 -0
  96. package/dist/http-client/fake.d.ts.map +1 -0
  97. package/dist/http-client/fake.js +95 -0
  98. package/dist/http-client/fake.js.map +1 -0
  99. package/dist/http-client/index.d.ts +7 -0
  100. package/dist/http-client/index.d.ts.map +1 -0
  101. package/dist/http-client/index.js +7 -0
  102. package/dist/http-client/index.js.map +1 -0
  103. package/dist/index.d.ts +4 -3
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/middleware/Pipeline.d.ts.map +1 -1
  108. package/dist/middleware/Pipeline.js +6 -3
  109. package/dist/middleware/Pipeline.js.map +1 -1
  110. package/dist/native/generated.d.ts +93 -36
  111. package/dist/native/generated.d.ts.map +1 -1
  112. package/dist/profiler/Profiler.d.ts.map +1 -1
  113. package/dist/profiler/Profiler.js +4 -1
  114. package/dist/profiler/Profiler.js.map +1 -1
  115. package/dist/profiler/client.generated.d.ts +5 -0
  116. package/dist/profiler/client.generated.d.ts.map +1 -0
  117. package/dist/profiler/client.generated.js +6 -0
  118. package/dist/profiler/client.generated.js.map +1 -0
  119. package/dist/profiler/collectors.d.ts +92 -13
  120. package/dist/profiler/collectors.d.ts.map +1 -1
  121. package/dist/profiler/collectors.js +106 -19
  122. package/dist/profiler/collectors.js.map +1 -1
  123. package/dist/profiler/endpoint.d.ts +2 -1
  124. package/dist/profiler/endpoint.d.ts.map +1 -1
  125. package/dist/profiler/endpoint.js +25 -1
  126. package/dist/profiler/endpoint.js.map +1 -1
  127. package/dist/profiler/index.d.ts +8 -4
  128. package/dist/profiler/index.d.ts.map +1 -1
  129. package/dist/profiler/index.js +11 -4
  130. package/dist/profiler/index.js.map +1 -1
  131. package/dist/profiler/middleware.d.ts.map +1 -1
  132. package/dist/profiler/middleware.js +76 -10
  133. package/dist/profiler/middleware.js.map +1 -1
  134. package/dist/profiler/preview.d.ts +17 -0
  135. package/dist/profiler/preview.d.ts.map +1 -0
  136. package/dist/profiler/preview.js +100 -0
  137. package/dist/profiler/preview.js.map +1 -0
  138. package/dist/profiler/toolbar.d.ts +15 -23
  139. package/dist/profiler/toolbar.d.ts.map +1 -1
  140. package/dist/profiler/toolbar.js +25 -91
  141. package/dist/profiler/toolbar.js.map +1 -1
  142. package/dist/profiler/types.d.ts +6 -1
  143. package/dist/profiler/types.d.ts.map +1 -1
  144. package/dist/router/Router.d.ts +6 -0
  145. package/dist/router/Router.d.ts.map +1 -1
  146. package/dist/router/Router.js +115 -12
  147. package/dist/router/Router.js.map +1 -1
  148. package/dist/rpc/RpcProvider.d.ts +6 -0
  149. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  150. package/dist/rpc/RpcProvider.js +4 -1
  151. package/dist/rpc/RpcProvider.js.map +1 -1
  152. package/dist/rpc/RpcRouter.d.ts +27 -14
  153. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  154. package/dist/rpc/RpcRouter.js +97 -40
  155. package/dist/rpc/RpcRouter.js.map +1 -1
  156. package/dist/scheduler/Scheduler.js +1 -1
  157. package/dist/scheduler/Scheduler.js.map +1 -1
  158. package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
  159. package/dist/scheduler/locks/RedisLockBackend.js +13 -2
  160. package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
  161. package/dist/security/CookieSigner.d.ts.map +1 -1
  162. package/dist/security/CookieSigner.js +32 -8
  163. package/dist/security/CookieSigner.js.map +1 -1
  164. package/dist/security/SignedUrl.d.ts.map +1 -1
  165. package/dist/security/SignedUrl.js +16 -19
  166. package/dist/security/SignedUrl.js.map +1 -1
  167. package/dist/server/Server.d.ts +44 -12
  168. package/dist/server/Server.d.ts.map +1 -1
  169. package/dist/server/Server.js +31 -10
  170. package/dist/server/Server.js.map +1 -1
  171. package/dist/server/nativeGrpc.d.ts +27 -0
  172. package/dist/server/nativeGrpc.d.ts.map +1 -0
  173. package/dist/server/nativeGrpc.js +47 -0
  174. package/dist/server/nativeGrpc.js.map +1 -0
  175. package/dist/server/nativeLimits.d.ts +57 -0
  176. package/dist/server/nativeLimits.d.ts.map +1 -0
  177. package/dist/server/nativeLimits.js +75 -0
  178. package/dist/server/nativeLimits.js.map +1 -0
  179. package/dist/session/Session.d.ts +12 -0
  180. package/dist/session/Session.d.ts.map +1 -1
  181. package/dist/session/Session.js +13 -4
  182. package/dist/session/Session.js.map +1 -1
  183. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  184. package/dist/session/SessionMiddleware.js +28 -9
  185. package/dist/session/SessionMiddleware.js.map +1 -1
  186. package/dist/session/config.d.ts +5 -0
  187. package/dist/session/config.d.ts.map +1 -1
  188. package/dist/session/config.js +8 -0
  189. package/dist/session/config.js.map +1 -1
  190. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  191. package/dist/session/drivers/MemoryDriver.js +26 -7
  192. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  193. package/dist/session/errors.d.ts +17 -0
  194. package/dist/session/errors.d.ts.map +1 -1
  195. package/dist/session/errors.js +21 -0
  196. package/dist/session/errors.js.map +1 -1
  197. package/dist/session/templateTags.d.ts +14 -0
  198. package/dist/session/templateTags.d.ts.map +1 -1
  199. package/dist/session/templateTags.js +45 -17
  200. package/dist/session/templateTags.js.map +1 -1
  201. package/dist/storage/StaticMiddleware.d.ts.map +1 -1
  202. package/dist/storage/StaticMiddleware.js +49 -27
  203. package/dist/storage/StaticMiddleware.js.map +1 -1
  204. package/dist/testing/ApiResponse.d.ts.map +1 -1
  205. package/dist/testing/ApiResponse.js +7 -1
  206. package/dist/testing/ApiResponse.js.map +1 -1
  207. package/dist/testing/TestClient.d.ts.map +1 -1
  208. package/dist/testing/TestClient.js +16 -4
  209. package/dist/testing/TestClient.js.map +1 -1
  210. package/dist/utils/Macroable.d.ts +1 -1
  211. package/dist/utils/Macroable.js +2 -2
  212. package/dist/utils/Macroable.js.map +1 -1
  213. package/events.darwin-arm64.node +0 -0
  214. package/events.darwin-x64.node +0 -0
  215. package/events.linux-arm64-gnu.node +0 -0
  216. package/events.linux-x64-gnu.node +0 -0
  217. package/events.win32-x64-msvc.node +0 -0
  218. package/index.darwin-arm64.node +0 -0
  219. package/index.darwin-x64.node +0 -0
  220. package/index.linux-arm64-gnu.node +0 -0
  221. package/index.linux-x64-gnu.node +0 -0
  222. package/index.win32-x64-msvc.node +0 -0
  223. package/package.json +11 -3
  224. package/scheduler.darwin-arm64.node +0 -0
  225. package/scheduler.darwin-x64.node +0 -0
  226. package/scheduler.linux-arm64-gnu.node +0 -0
  227. package/scheduler.linux-x64-gnu.node +0 -0
  228. package/scheduler.win32-x64-msvc.node +0 -0
  229. package/src/Codemods.ts +314 -3
  230. package/src/HttpKernel.ts +18 -7
  231. package/src/Ignitor.ts +58 -26
  232. package/src/bodyparser/BodyParserMiddleware.ts +45 -16
  233. package/src/bodyparser/MultipartFile.ts +46 -67
  234. package/src/bodyparser/parseSize.ts +17 -17
  235. package/src/bodyparser/qsParse.ts +96 -26
  236. package/src/codemods/source.ts +303 -0
  237. package/src/console/prompts.ts +3 -1
  238. package/src/container/Container.ts +97 -81
  239. package/src/events/Emitter.ts +37 -10
  240. package/src/events/helix/helpers.ts +24 -19
  241. package/src/graphql/GraphQLEngine.ts +91 -14
  242. package/src/helpers/MessageBuilder.ts +4 -2
  243. package/src/helpers/errors.ts +16 -0
  244. package/src/helpers/napi-loader.ts +3 -2
  245. package/src/http/Exception.ts +17 -2
  246. package/src/http/HttpContext.ts +34 -5
  247. package/src/http/RedirectBuilder.ts +51 -37
  248. package/src/http/Request.ts +102 -58
  249. package/src/http/Response.ts +15 -7
  250. package/src/http/SseStream.ts +6 -7
  251. package/src/http/permissions.ts +47 -0
  252. package/src/http-client/HttpClient.ts +505 -0
  253. package/src/http-client/addresses.ts +58 -0
  254. package/src/http-client/fake.ts +141 -0
  255. package/src/http-client/index.ts +21 -0
  256. package/src/index.ts +6 -1
  257. package/src/middleware/Pipeline.ts +7 -3
  258. package/src/native/generated.ts +106 -48
  259. package/src/profiler/Profiler.ts +4 -1
  260. package/src/profiler/client/toolbar.css +595 -0
  261. package/src/profiler/client/toolbar.js +1299 -0
  262. package/src/profiler/client.generated.ts +7 -0
  263. package/src/profiler/collectors.ts +156 -19
  264. package/src/profiler/endpoint.ts +27 -1
  265. package/src/profiler/index.ts +17 -3
  266. package/src/profiler/middleware.ts +74 -9
  267. package/src/profiler/preview.ts +104 -0
  268. package/src/profiler/toolbar.ts +28 -91
  269. package/src/profiler/types.ts +6 -1
  270. package/src/router/Router.ts +136 -13
  271. package/src/rpc/RpcProvider.ts +10 -1
  272. package/src/rpc/RpcRouter.ts +129 -45
  273. package/src/scheduler/Scheduler.ts +1 -1
  274. package/src/scheduler/locks/RedisLockBackend.ts +12 -4
  275. package/src/security/CookieSigner.ts +30 -8
  276. package/src/security/SignedUrl.ts +18 -17
  277. package/src/server/Server.ts +60 -12
  278. package/src/server/nativeGrpc.ts +59 -0
  279. package/src/server/nativeLimits.ts +111 -0
  280. package/src/session/Session.ts +29 -4
  281. package/src/session/SessionMiddleware.ts +30 -10
  282. package/src/session/config.ts +7 -0
  283. package/src/session/drivers/MemoryDriver.ts +26 -7
  284. package/src/session/errors.ts +31 -0
  285. package/src/session/templateTags.ts +57 -0
  286. package/src/storage/StaticMiddleware.ts +46 -27
  287. package/src/testing/ApiResponse.ts +7 -1
  288. package/src/testing/TestClient.ts +14 -5
  289. package/src/utils/Macroable.ts +2 -2
  290. package/scripts/build-napi-types.mjs +0 -69
  291. package/scripts/copy-napi.mjs +0 -121
  292. package/scripts/generate-mime.mjs +0 -119
  293. package/scripts/generate-napi-types.mjs +0 -187
package/src/Codemods.ts CHANGED
@@ -1,9 +1,36 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
+ import {
4
+ appendToLiteral,
5
+ findCall,
6
+ findExportDefaultObject,
7
+ isSingleExpression,
8
+ matchClose,
9
+ properties,
10
+ segments,
11
+ } from './codemods/source.js'
3
12
  import { render as renderTemplate, type StubState } from './stubs/template.js'
4
13
 
5
14
  export type { StubState }
6
15
 
16
+ /** The environments a preload (or provider) entry can be scoped to. */
17
+ export type PreloadEnvironment = 'web' | 'console' | 'test' | 'repl'
18
+
19
+ const PRELOAD_ENVIRONMENTS: readonly PreloadEnvironment[] = ['web', 'console', 'test', 'repl']
20
+
21
+ /** Validation rules a configure hook adds to `start/env.ts`. */
22
+ export interface EnvValidationNode {
23
+ /** Written once, as a banner comment above the rules this call adds. */
24
+ leadingComment?: string
25
+ /** Variable name → rule source, e.g. `{ REDIS_PORT: 'Env.schema.number()' }`. */
26
+ variables: Record<string, string>
27
+ /**
28
+ * Replace a rule the file already has. Off by default, so a rule the user
29
+ * edited survives a re-run; configure hooks pass their `--force` flag.
30
+ */
31
+ overwrite?: boolean
32
+ }
33
+
7
34
  export interface Codemods {
8
35
  addProvider(importPath: string): Promise<void>
9
36
  addMetaFile(pattern: string, reloadServer?: boolean): Promise<void>
@@ -16,7 +43,12 @@ export interface Codemods {
16
43
  options?: { force?: boolean },
17
44
  ): Promise<GeneratedStub>
18
45
  registerCommand(importPath: string): Promise<void>
19
- registerMiddleware(importPath: string, options?: { tier?: 'server' | 'router' }): Promise<void>
46
+ registerMiddleware(
47
+ importPath: string,
48
+ options?: { tier?: 'server' | 'router'; position?: MiddlewarePosition },
49
+ ): Promise<void>
50
+ defineEnvValidations(definition: EnvValidationNode): Promise<string[]>
51
+ addPreloadFile(modulePath: string, environments?: PreloadEnvironment[]): Promise<void>
20
52
  }
21
53
 
22
54
  /** What a stub may interpolate. Scalars only — a stub is text, not a program. */
@@ -102,6 +134,40 @@ function findTierBlock(
102
134
  return { start, end: start + closeMatch.index }
103
135
  }
104
136
 
137
+ /**
138
+ * Where a middleware entry goes in its tier's array: at the end (`after`, the
139
+ * default, as the upstream assembler does) or at the head (`before`). Order is
140
+ * execution order, so a middleware that needs another one's work — a CSRF
141
+ * guard reading the session — is appended after it.
142
+ */
143
+ export type MiddlewarePosition = 'before' | 'after'
144
+
145
+ /**
146
+ * Insert `text` after the last element of the array whose contents span
147
+ * `block`, adding the comma the previous element may lack. A trailing line
148
+ * comment is stepped over so the comma never lands inside it.
149
+ */
150
+ function appendToArray(
151
+ content: string,
152
+ block: { start: number; end: number },
153
+ text: string,
154
+ ): string {
155
+ const inner = content.slice(block.start, block.end)
156
+ const trimmed = inner.trimEnd()
157
+ const insertAt = block.start + trimmed.length
158
+ if (trimmed.trim() === '' || trimmed.endsWith(',')) {
159
+ return `${content.slice(0, insertAt)}${text}${content.slice(insertAt)}`
160
+ }
161
+ const lineStart = trimmed.lastIndexOf('\n') + 1
162
+ const commentAt = trimmed.indexOf('//', lineStart)
163
+ const code = (commentAt === -1 ? trimmed : trimmed.slice(0, commentAt)).trimEnd()
164
+ if (code.trim() === '' || code.endsWith(',')) {
165
+ return `${content.slice(0, insertAt)}${text}${content.slice(insertAt)}`
166
+ }
167
+ const commaAt = block.start + code.length
168
+ return `${content.slice(0, commaAt)},${content.slice(commaAt, insertAt)}${text}${content.slice(insertAt)}`
169
+ }
170
+
105
171
  /**
106
172
  * Detect line ending (CRLF on Windows checkouts, LF elsewhere) so codemods preserve the file's existing convention.
107
173
  * Majority-based: a stray CRLF in an otherwise-LF file must not flip the whole insertion to CRLF.
@@ -128,6 +194,76 @@ function assertCanonicallyInside(root: string, filePath: string, displayName: st
128
194
  }
129
195
  }
130
196
 
197
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
198
+
199
+ /**
200
+ * A module path goes between the quotes of an `import('...')`; anything that
201
+ * could close them, escape or end the line is refused rather than escaped.
202
+ */
203
+ const SAFE_MODULE_PATH = /^[^'"`\\\r\n\u2028\u2029]+$/
204
+
205
+ const BANNER_RULE = '|----------------------------------------------------------'
206
+
207
+ /** The rules of `definition`, refused before anything is written when unsafe. */
208
+ function assertEnvValidations(definition: EnvValidationNode): void {
209
+ const comment = definition.leadingComment
210
+ if (comment !== undefined && (/[\r\n\u2028\u2029]/.test(comment) || comment.includes('*/'))) {
211
+ throw new Error(
212
+ '[configure] defineEnvValidations: the leading comment must be one line without `*/`',
213
+ )
214
+ }
215
+ for (const [name, rule] of Object.entries(definition.variables)) {
216
+ if (!ENV_NAME.test(name)) {
217
+ throw new Error(
218
+ `[configure] defineEnvValidations: '${name}' is not an environment variable name`,
219
+ )
220
+ }
221
+ if (typeof rule !== 'string' || !isSingleExpression(rule)) {
222
+ throw new Error(
223
+ `[configure] defineEnvValidations: the rule for ${name} is not one complete expression: ${String(rule)}`,
224
+ )
225
+ }
226
+ }
227
+ }
228
+
229
+ /** The `{` and `}` of the schema object passed to `Env.create`. */
230
+ function locateEnvSchema(content: string): { open: number; close: number } {
231
+ const paren = findCall(content, 'Env.create')
232
+ const parenClose = paren === -1 ? -1 : matchClose(content, paren)
233
+ if (paren === -1 || parenClose === -1) {
234
+ throw new Error(
235
+ '[configure] Could not find an Env.create(...) call in start/env.ts — add the validations by hand.',
236
+ )
237
+ }
238
+ const open = segments(content, paren + 1, parenClose)[1]?.codeStart ?? -1
239
+ const close = content[open] === '{' ? matchClose(content, open) : -1
240
+ if (close === -1) {
241
+ throw new Error(
242
+ '[configure] The second argument of Env.create in start/env.ts is not an object literal — add the validations by hand.',
243
+ )
244
+ }
245
+ return { open, close }
246
+ }
247
+
248
+ /** The `{` and `}` of the object `reamrc.ts` exports (through `defineConfig` or bare). */
249
+ function locateRcObject(content: string): { open: number; close: number } | null {
250
+ const paren = findCall(content, 'defineConfig')
251
+ let open = -1
252
+ if (paren !== -1) {
253
+ const parenClose = matchClose(content, paren)
254
+ if (parenClose !== -1) open = segments(content, paren + 1, parenClose)[0]?.codeStart ?? -1
255
+ } else {
256
+ open = findExportDefaultObject(content)
257
+ }
258
+ const close = open !== -1 && content[open] === '{' ? matchClose(content, open) : -1
259
+ return close === -1 ? null : { open, close }
260
+ }
261
+
262
+ /** The module an rc array element lazily imports. */
263
+ function importedModule(element: string): string | undefined {
264
+ return /import\s*\(\s*(['"])([^'"]*)\1\s*\)/.exec(element)?.[2]
265
+ }
266
+
131
267
  export function createCodemods(options?: { force?: boolean; cwd?: string }): Codemods {
132
268
  const force = options?.force ?? false
133
269
  const root = path.resolve(options?.cwd ?? process.cwd())
@@ -467,8 +603,14 @@ export function createCodemods(options?: { force?: boolean; cwd?: string }): Cod
467
603
  */
468
604
  async registerMiddleware(
469
605
  importPath: string,
470
- options?: { tier?: MiddlewareTier },
606
+ options?: { tier?: MiddlewareTier; position?: MiddlewarePosition },
471
607
  ): Promise<void> {
608
+ const position = options?.position ?? 'after'
609
+ if (position !== 'before' && position !== 'after') {
610
+ throw new Error(
611
+ `[configure] Invalid middleware position: '${String(position)}'. Expected 'before' or 'after'.`,
612
+ )
613
+ }
472
614
  const requestedTier = options?.tier ?? 'router'
473
615
  if (!isMiddlewareTier(requestedTier)) {
474
616
  throw new Error(
@@ -513,8 +655,177 @@ export function createCodemods(options?: { force?: boolean; cwd?: string }): Cod
513
655
 
514
656
  const eol = detectLineEnding(content)
515
657
  const entry = ` () => import('${importPath}'),`
516
- content = `${content.slice(0, targetBlock.start)}${eol}${entry}${content.slice(targetBlock.start)}`
658
+ content =
659
+ position === 'before'
660
+ ? `${content.slice(0, targetBlock.start)}${eol}${entry}${content.slice(targetBlock.start)}`
661
+ : appendToArray(content, targetBlock, `${eol}${entry}`)
517
662
  fs.writeFileSync(kernelPath, content)
518
663
  },
664
+
665
+ /**
666
+ * Add validation rules to the schema `start/env.ts` passes to
667
+ * `Env.create`, so the variables a package writes to `.env` are checked
668
+ * at boot like the app's own.
669
+ *
670
+ * A rule already in the file is left alone, unless `overwrite` is set:
671
+ * the user may have tightened it. Returns the variables kept that way
672
+ * with a rule different from the one asked for, so the caller can say
673
+ * so. The banner comment is written only when the first new rule comes
674
+ * before any variable the file already had.
675
+ */
676
+ async defineEnvValidations(definition: EnvValidationNode): Promise<string[]> {
677
+ const envPath = path.join(root, 'start', 'env.ts')
678
+ if (!fs.existsSync(envPath)) {
679
+ throw new Error(
680
+ `[configure] start/env.ts not found — cannot add the validations for ${Object.keys(definition.variables).join(', ')}.`,
681
+ )
682
+ }
683
+ assertCanonicallyInside(root, envPath, 'start/env.ts')
684
+ assertEnvValidations(definition)
685
+
686
+ const original = fs.readFileSync(envPath, 'utf8')
687
+ const eol = detectLineEnding(original)
688
+ let content = original
689
+ const schema = locateEnvSchema(content)
690
+ const existing = new Map(
691
+ properties(content, schema.open, schema.close).map((property) => [property.key, property]),
692
+ )
693
+
694
+ const kept: string[] = []
695
+ const replacements: Array<{ start: number; end: number; text: string }> = []
696
+ const additions: Array<[string, string]> = []
697
+ let shouldAddComment = true
698
+ let decorate = false
699
+ for (const [name, rule] of Object.entries(definition.variables)) {
700
+ const property = existing.get(name)
701
+ if (property === undefined) {
702
+ if (additions.length === 0) decorate = shouldAddComment
703
+ shouldAddComment = false
704
+ additions.push([name, rule])
705
+ continue
706
+ }
707
+ shouldAddComment = false
708
+ const current = content.slice(property.valueStart, property.valueEnd)
709
+ if (definition.overwrite) {
710
+ replacements.push({ start: property.valueStart, end: property.valueEnd, text: rule })
711
+ } else if (current.replace(/\s+/g, '') !== rule.replace(/\s+/g, '')) {
712
+ kept.push(name)
713
+ }
714
+ }
715
+
716
+ for (const { start, end, text } of replacements.sort((a, b) => b.start - a.start)) {
717
+ content = `${content.slice(0, start)}${text}${content.slice(end)}`
718
+ }
719
+
720
+ if (additions.length > 0) {
721
+ const { open, close } = locateEnvSchema(content)
722
+ const comment = definition.leadingComment
723
+ content = appendToLiteral(
724
+ content,
725
+ open,
726
+ close,
727
+ eol,
728
+ (indent) => [
729
+ ...(decorate && comment !== undefined
730
+ ? [
731
+ `${indent}/*`,
732
+ `${indent}${BANNER_RULE}`,
733
+ `${indent}| ${comment}`,
734
+ `${indent}${BANNER_RULE}`,
735
+ `${indent}*/`,
736
+ ]
737
+ : []),
738
+ ...additions.map(([name, rule]) => `${indent}${name}: ${rule},`),
739
+ ],
740
+ decorate,
741
+ )
742
+ }
743
+
744
+ if (content !== original) fs.writeFileSync(envPath, content)
745
+ return kept
746
+ },
747
+
748
+ /**
749
+ * Register a preload file in `reamrc.ts`, appended to `preloads` (the
750
+ * array is created when absent). Listing only some environments writes
751
+ * a `{ file, environment }` entry; none, or all of them, a plain lazy
752
+ * import. A module already in `preloads` is left as it is.
753
+ */
754
+ async addPreloadFile(modulePath: string, environments?: PreloadEnvironment[]): Promise<void> {
755
+ if (!SAFE_MODULE_PATH.test(modulePath)) {
756
+ throw new Error(`[configure] Unsafe preload module path: ${JSON.stringify(modulePath)}`)
757
+ }
758
+ for (const environment of environments ?? []) {
759
+ if (!PRELOAD_ENVIRONMENTS.includes(environment)) {
760
+ throw new Error(
761
+ `[configure] Unknown environment '${String(environment)}' for preload ${modulePath}. Expected one of ${PRELOAD_ENVIRONMENTS.join(', ')}.`,
762
+ )
763
+ }
764
+ }
765
+
766
+ const rcPath = path.join(root, 'reamrc.ts')
767
+ if (!fs.existsSync(rcPath)) {
768
+ throw new Error(
769
+ `[configure] reamrc.ts not found — cannot register preload ${modulePath}. Run 'ream new' first.`,
770
+ )
771
+ }
772
+ assertCanonicallyInside(root, rcPath, 'reamrc.ts')
773
+
774
+ const content = fs.readFileSync(rcPath, 'utf8')
775
+ const eol = detectLineEnding(content)
776
+ const config = locateRcObject(content)
777
+ if (config === null) {
778
+ throw new Error(
779
+ `[configure] Could not find 'defineConfig({ ... })' in reamrc.ts — preload ${modulePath} not added.`,
780
+ )
781
+ }
782
+
783
+ // Scoped only when it actually leaves an environment out.
784
+ const scope = PRELOAD_ENVIRONMENTS.some(
785
+ (environment) => environments !== undefined && !environments.includes(environment),
786
+ )
787
+ ? environments
788
+ : undefined
789
+ const entry = (indent: string): string[] =>
790
+ scope
791
+ ? [
792
+ `${indent}{`,
793
+ `${indent} file: () => import('${modulePath}'),`,
794
+ `${indent} environment: [${scope.map((e) => `'${e}'`).join(', ')}],`,
795
+ `${indent}},`,
796
+ ]
797
+ : [`${indent}() => import('${modulePath}'),`]
798
+
799
+ const preloads = properties(content, config.open, config.close).find(
800
+ (property) => property.key === 'preloads',
801
+ )
802
+ if (preloads === undefined) {
803
+ fs.writeFileSync(
804
+ rcPath,
805
+ appendToLiteral(content, config.open, config.close, eol, (indent) => [
806
+ `${indent}preloads: [`,
807
+ ...entry(`${indent} `),
808
+ `${indent}],`,
809
+ ]),
810
+ )
811
+ return
812
+ }
813
+
814
+ const arrayClose =
815
+ content[preloads.valueStart] === '[' ? matchClose(content, preloads.valueStart) : -1
816
+ if (arrayClose === -1) {
817
+ throw new Error(
818
+ `[configure] reamrc.ts declares preloads as something other than an array literal — add ${modulePath} by hand.`,
819
+ )
820
+ }
821
+ const registered = segments(content, preloads.valueStart + 1, arrayClose).some(
822
+ (element) => importedModule(content.slice(element.codeStart, element.end)) === modulePath,
823
+ )
824
+ if (registered) return
825
+ fs.writeFileSync(
826
+ rcPath,
827
+ appendToLiteral(content, preloads.valueStart, arrayClose, eol, entry),
828
+ )
829
+ },
519
830
  }
520
831
  }
package/src/HttpKernel.ts CHANGED
@@ -200,7 +200,7 @@ export function createHttpKernel(
200
200
  // read decorator metadata via Reflect. Absent for inline-arrow
201
201
  // route handlers — that's intentional, decorators only apply
202
202
  // to controller methods.
203
- controller: match.route.controller?.target.prototype as object | undefined,
203
+ controller: controllerPrototype(match.route.controller?.target),
204
204
  action: match.route.controller?.method,
205
205
  }
206
206
  : { pattern: '', middleware: [] }
@@ -212,6 +212,7 @@ export function createHttpKernel(
212
212
  // value bound for this request cannot be seen by any other.
213
213
  const resolver = config.container?.createResolver()
214
214
  const ctx = new HttpContext(correlationId, reqData, match?.params ?? {}, routeInfo, resolver)
215
+ ctx.subdomains = match?.subdomains ?? {}
215
216
  // What Adonis binds on every request's resolver: the context itself, so a
216
217
  // controller or service can take `HttpContext` as a constructor dependency.
217
218
  resolver?.bindValue(HttpContext, ctx)
@@ -453,18 +454,25 @@ function createControllerHandler(
453
454
  // `HttpContext` as a dependency must get this request's, not a stale one.
454
455
  const resolve = ctx.containerResolver ?? container
455
456
  const instance = resolve ? await resolve.make(controller.target) : new controller.target()
456
- const method = (instance as Record<string, (ctx: HttpContext) => Promise<void> | void>)[
457
- controller.method
458
- ]
457
+ const method: unknown =
458
+ typeof instance === 'object' && instance !== null
459
+ ? Reflect.get(instance, controller.method)
460
+ : undefined
459
461
  if (typeof method !== 'function') {
460
462
  throw new Error(
461
463
  `Controller method '${controller.method}' not found on ${controller.target.name}`,
462
464
  )
463
465
  }
464
- await method.call(instance, ctx)
466
+ await Reflect.apply(method, instance, [ctx])
465
467
  }
466
468
  }
467
469
 
470
+ /** A controller class's prototype, where its decorators keep their metadata. */
471
+ function controllerPrototype(target: AnyConstructor | undefined): object | undefined {
472
+ const prototype: unknown = target?.prototype
473
+ return typeof prototype === 'object' && prototype !== null ? prototype : undefined
474
+ }
475
+
468
476
  /**
469
477
  * Read @Guard, @Role, @Permission metadata from a controller method.
470
478
  * Returns empty arrays if no decorators are present.
@@ -554,8 +562,10 @@ function reconcileRepresentation(
554
562
  /**
555
563
  * Append the development script to an HTML body.
556
564
  *
557
- * Only HTML, and never a stream: appending to an SSE or a download would
558
- * corrupt it, and a page is the only thing that can act on the script anyway.
565
+ * Only HTML, and never a stream or a base64-encoded body (`sendBuffer`, a
566
+ * buffered download): appending to any of them corrupts it — Rust fails to
567
+ * decode the base64 and the client receives the encoded text — and a page is
568
+ * the only thing that can act on the script anyway.
559
569
  * Before `</body>` when there is one, so the script is inside the document
560
570
  * rather than trailing after it.
561
571
  */
@@ -567,6 +577,7 @@ function withDevScript(
567
577
  ): string {
568
578
  if (script === undefined || streamId !== undefined || body === '') return body
569
579
  if (!(headers['content-type'] ?? '').includes('text/html')) return body
580
+ if (headers['x-ream-body-encoding'] === 'base64') return body
570
581
  const closing = body.lastIndexOf('</body>')
571
582
  return closing === -1 ? body + script : body.slice(0, closing) + script + body.slice(closing)
572
583
  }
package/src/Ignitor.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { Application } from './Application.js'
17
+ import type { BodyParserConfig } from './bodyparser/BodyParserMiddleware.js'
17
18
  import type { Console } from './console/Console.js'
18
19
  import type { CommandLoader, Kernel as ConsoleKernelInstance } from './console/Kernel.js'
19
20
  import { type CommandClass, isCommandClass } from './console/types.js'
@@ -51,13 +52,20 @@ import {
51
52
  import { Router } from './router/Router.js'
52
53
  import { CookieSigner } from './security/CookieSigner.js'
53
54
  import { SignedUrl } from './security/SignedUrl.js'
54
- import { type GrpcHandler, Server } from './server/Server.js'
55
+ import { cancellableGrpc, type NativeGrpcHandler } from './server/nativeGrpc.js'
56
+ import {
57
+ type NativeBodyLimits,
58
+ type NativeConnectionConfig,
59
+ nativeBodyLimits,
60
+ nativeConnectionConfig,
61
+ } from './server/nativeLimits.js'
62
+ import { type GrpcLimits, Server } from './server/Server.js'
55
63
  import { clearApp, setApp } from './services/app.js'
56
64
  import { clearEncryption, setEncryption } from './services/encryption.js'
57
65
  import { clearLogger, setLogger } from './services/logger.js'
58
66
  import { clearRouter, setRouter } from './services/router.js'
59
67
  import { clearServer, setServer } from './services/server.js'
60
- import { registerSessionTemplateTags } from './session/templateTags.js'
68
+ import { installSessionTemplateTags } from './session/templateTags.js'
61
69
 
62
70
  /** Application environment. */
63
71
  /**
@@ -269,7 +277,9 @@ export function defineConfig(config: ReamrcConfig): ReamrcConfig {
269
277
  export interface HyperServerLike {
270
278
  onRequest(callback: (request: HttpKernelRequest) => Promise<HttpKernelResponse>): void
271
279
  /** Optional — present on the real `HyperServer`; required once `server.grpc()` is used. */
272
- onGrpc?(callback: GrpcHandler): void
280
+ onGrpc?(callback: NativeGrpcHandler, limits?: GrpcLimits): void
281
+ /** Optional — reports the calls a client gave up on; without it no call's signal aborts. */
282
+ onGrpcCancel?(callback: (id: number) => void): void
273
283
  /**
274
284
  * Optional — present on the real `HyperServer` (Rust-backed), absent on
275
285
  * trivial test mocks. Ignitor only calls it when a shield config is set.
@@ -279,6 +289,10 @@ export interface HyperServerLike {
279
289
  configureTrustedProxies?(cidrs: string[]): void
280
290
  /** Optional — same lifecycle as `configureShield`. */
281
291
  configureRateLimit?(config: { max: number; windowSecs: number } | null): void
292
+ /** Optional — same lifecycle as `configureShield`. */
293
+ configureBodyLimits?(config: NativeBodyLimits): void
294
+ /** Optional — same lifecycle as `configureShield`. */
295
+ configureConnections?(config: NativeConnectionConfig): void
282
296
  listen(): Promise<void>
283
297
  port(): Promise<number>
284
298
  close(): Promise<void>
@@ -287,6 +301,11 @@ export interface HyperServerLike {
287
301
  // a clean `STREAMING_UNSUPPORTED` error when called on such a host.
288
302
  registerStream?(streamId: string): Promise<boolean>
289
303
  writeStream?(streamId: string, chunk: string): Promise<boolean>
304
+ /**
305
+ * Byte-exact writes. Without it a download, `response.stream()` or a static
306
+ * file is buffered whole and shipped base64-encoded in one response.
307
+ */
308
+ writeStreamBytes?(streamId: string, chunk: Uint8Array): Promise<boolean>
290
309
  closeStream?(streamId: string): Promise<boolean>
291
310
  onStreamDisconnect?(streamId: string, callback: () => void): void
292
311
  }
@@ -684,26 +703,12 @@ export class Ignitor {
684
703
 
685
704
  /**
686
705
  * Give the template engine `@error` / `@errors` / `@inputError` /
687
- * `@flashMessage`, as AdonisJS's session plugin does.
688
- *
689
- * The engine is duck-typed out of the container so ream carries no dependency
690
- * on it, and an app with no template layer simply skips this.
706
+ * `@flashMessage`, as AdonisJS's session plugin does — through the engine
707
+ * package's module singleton, guarded by `usingInker` (see
708
+ * `installSessionTemplateTags`). An app with no template layer skips this.
691
709
  */
692
710
  async #registerSessionTemplateTags(): Promise<void> {
693
- for (const token of ['inker', 'view']) {
694
- if (!this.#app.container.has(token)) continue
695
- const binding: unknown = await this.#app.container.resolve(token)
696
- for (const candidate of [binding, Reflect.get(Object(binding), '_templates')]) {
697
- const register = Reflect.get(Object(candidate), 'registerTag')
698
- if (typeof register !== 'function') continue
699
- registerSessionTemplateTags({
700
- registerTag: (tag) => {
701
- register.call(candidate, tag)
702
- },
703
- })
704
- return
705
- }
706
- }
711
+ await installSessionTemplateTags(this.#app.usingInker === true)
707
712
  }
708
713
 
709
714
  async #phaseRegister(): Promise<void> {
@@ -874,6 +879,12 @@ export class Ignitor {
874
879
  return {
875
880
  registerStream: server.registerStream.bind(server),
876
881
  writeStream: server.writeStream.bind(server),
882
+ // The byte channel is what lets a download be streamed rather
883
+ // than read whole into memory; a host without it still gets SSE.
884
+ writeStreamBytes:
885
+ typeof server.writeStreamBytes === 'function'
886
+ ? server.writeStreamBytes.bind(server)
887
+ : undefined,
877
888
  closeStream: server.closeStream.bind(server),
878
889
  onStreamDisconnect: server.onStreamDisconnect.bind(server),
879
890
  }
@@ -914,7 +925,9 @@ export class Ignitor {
914
925
  { hint: 'Use the native HyperServer, or a test server that implements onGrpc().' },
915
926
  )
916
927
  }
917
- this.#_httpServer.onGrpc(grpc)
928
+ const calls = cancellableGrpc(grpc)
929
+ this.#_httpServer.onGrpcCancel?.(calls.onCancel)
930
+ this.#_httpServer.onGrpc(calls.onCall, this.#server.getGrpcLimits())
918
931
  }
919
932
  // Pre-resolve client IPs in Rust from the trusted-proxy CIDRs before
920
933
  // listen. Security filtering itself lives in @c9up/blackhole.
@@ -925,6 +938,19 @@ export class Ignitor {
925
938
  ) {
926
939
  this.#_httpServer.configureTrustedProxies([...trustedProxies])
927
940
  }
941
+ // Body limits from `config/bodyparser.ts`, enforced while the native
942
+ // server reads a body: nothing past them is ever buffered.
943
+ const bodyparser = this.#app.config.get<BodyParserConfig>('bodyparser')
944
+ if (bodyparser && typeof this.#_httpServer.configureBodyLimits === 'function') {
945
+ this.#_httpServer.configureBodyLimits(nativeBodyLimits(bodyparser))
946
+ }
947
+ // AdonisJS `config/app.ts` → `http: { headersTimeout, requestTimeout,
948
+ // keepAliveTimeout }`, plus `maxConnections`.
949
+ if (typeof this.#_httpServer.configureConnections === 'function') {
950
+ this.#_httpServer.configureConnections(
951
+ nativeConnectionConfig(this.#app.config.get<unknown>('app.http')),
952
+ )
953
+ }
928
954
  await this.#_httpServer.listen()
929
955
 
930
956
  // Whoever opened the socket closes it. `app.terminate()` runs the
@@ -1006,7 +1032,7 @@ export class Ignitor {
1006
1032
  'File change detected — a process restart is required to apply it (`ream dev` restarts automatically).',
1007
1033
  severity: 'info',
1008
1034
  timestamp: new Date().toISOString(),
1009
- } as ErrorEvent)
1035
+ })
1010
1036
  },
1011
1037
  logger: {
1012
1038
  info: (msg) =>
@@ -1016,7 +1042,7 @@ export class Ignitor {
1016
1042
  message: msg,
1017
1043
  severity: 'info',
1018
1044
  timestamp: new Date().toISOString(),
1019
- } as ErrorEvent),
1045
+ }),
1020
1046
  },
1021
1047
  })
1022
1048
  }
@@ -1399,7 +1425,13 @@ export class Ignitor {
1399
1425
  const events = this.#app.container.has('events')
1400
1426
  ? await this.#app.container.resolve<Emitter>('events')
1401
1427
  : { on: () => undefined }
1402
- this.#profiler = createProfiler(events)
1428
+ // Imported here, at start, rather than at the top: the barrel imports this
1429
+ // module, and its `VERSION` is what the release bump rewrites.
1430
+ const { VERSION } = await import('./index.js')
1431
+ this.#profiler = createProfiler(events, [], {
1432
+ version: VERSION,
1433
+ environment: process.env.REAM_ENV ?? process.env.NODE_ENV ?? 'development',
1434
+ })
1403
1435
  }
1404
1436
 
1405
1437
  #profiler?: Profiler
@@ -1453,7 +1485,7 @@ export class Ignitor {
1453
1485
  message,
1454
1486
  severity: kind === 'error' ? 'critical' : 'info',
1455
1487
  timestamp: new Date().toISOString(),
1456
- } as ErrorEvent)
1488
+ })
1457
1489
  }
1458
1490
  }
1459
1491