@c9up/ream 0.2.29 → 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 (220) hide show
  1. package/README.md +5 -6
  2. package/dist/Codemods.d.ts +8 -0
  3. package/dist/Codemods.d.ts.map +1 -1
  4. package/dist/Codemods.js +29 -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 +5 -0
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +12 -1
  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 +38 -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 +10 -0
  26. package/dist/bodyparser/qsParse.d.ts.map +1 -1
  27. package/dist/bodyparser/qsParse.js +27 -12
  28. package/dist/bodyparser/qsParse.js.map +1 -1
  29. package/dist/console/prompts.d.ts.map +1 -1
  30. package/dist/console/prompts.js +3 -1
  31. package/dist/console/prompts.js.map +1 -1
  32. package/dist/container/Container.d.ts.map +1 -1
  33. package/dist/container/Container.js +61 -71
  34. package/dist/container/Container.js.map +1 -1
  35. package/dist/events/Emitter.d.ts +12 -0
  36. package/dist/events/Emitter.d.ts.map +1 -1
  37. package/dist/events/Emitter.js +36 -9
  38. package/dist/events/Emitter.js.map +1 -1
  39. package/dist/events/helix/helpers.d.ts.map +1 -1
  40. package/dist/events/helix/helpers.js +19 -9
  41. package/dist/events/helix/helpers.js.map +1 -1
  42. package/dist/graphql/GraphQLEngine.d.ts +8 -0
  43. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  44. package/dist/graphql/GraphQLEngine.js +77 -11
  45. package/dist/graphql/GraphQLEngine.js.map +1 -1
  46. package/dist/helpers/MessageBuilder.d.ts +4 -2
  47. package/dist/helpers/MessageBuilder.d.ts.map +1 -1
  48. package/dist/helpers/MessageBuilder.js +4 -2
  49. package/dist/helpers/MessageBuilder.js.map +1 -1
  50. package/dist/helpers/errors.d.ts +11 -0
  51. package/dist/helpers/errors.d.ts.map +1 -0
  52. package/dist/helpers/errors.js +17 -0
  53. package/dist/helpers/errors.js.map +1 -0
  54. package/dist/helpers/napi-loader.d.ts.map +1 -1
  55. package/dist/helpers/napi-loader.js +3 -2
  56. package/dist/helpers/napi-loader.js.map +1 -1
  57. package/dist/http/HttpContext.d.ts +20 -2
  58. package/dist/http/HttpContext.d.ts.map +1 -1
  59. package/dist/http/HttpContext.js +10 -4
  60. package/dist/http/HttpContext.js.map +1 -1
  61. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  62. package/dist/http/RedirectBuilder.js +4 -0
  63. package/dist/http/RedirectBuilder.js.map +1 -1
  64. package/dist/http/Request.d.ts.map +1 -1
  65. package/dist/http/Request.js +15 -29
  66. package/dist/http/Request.js.map +1 -1
  67. package/dist/http/Response.d.ts +7 -4
  68. package/dist/http/Response.d.ts.map +1 -1
  69. package/dist/http/Response.js +14 -6
  70. package/dist/http/Response.js.map +1 -1
  71. package/dist/http/permissions.d.ts +14 -8
  72. package/dist/http/permissions.d.ts.map +1 -1
  73. package/dist/http/permissions.js +26 -16
  74. package/dist/http/permissions.js.map +1 -1
  75. package/dist/index.d.ts +2 -2
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/middleware/Pipeline.js +1 -1
  80. package/dist/middleware/Pipeline.js.map +1 -1
  81. package/dist/native/generated.d.ts +16 -30
  82. package/dist/native/generated.d.ts.map +1 -1
  83. package/dist/profiler/Profiler.d.ts.map +1 -1
  84. package/dist/profiler/Profiler.js +4 -1
  85. package/dist/profiler/Profiler.js.map +1 -1
  86. package/dist/profiler/client.generated.d.ts +5 -0
  87. package/dist/profiler/client.generated.d.ts.map +1 -0
  88. package/dist/profiler/client.generated.js +6 -0
  89. package/dist/profiler/client.generated.js.map +1 -0
  90. package/dist/profiler/collectors.d.ts +92 -13
  91. package/dist/profiler/collectors.d.ts.map +1 -1
  92. package/dist/profiler/collectors.js +106 -19
  93. package/dist/profiler/collectors.js.map +1 -1
  94. package/dist/profiler/endpoint.d.ts +2 -1
  95. package/dist/profiler/endpoint.d.ts.map +1 -1
  96. package/dist/profiler/endpoint.js +25 -1
  97. package/dist/profiler/endpoint.js.map +1 -1
  98. package/dist/profiler/index.d.ts +8 -4
  99. package/dist/profiler/index.d.ts.map +1 -1
  100. package/dist/profiler/index.js +11 -4
  101. package/dist/profiler/index.js.map +1 -1
  102. package/dist/profiler/middleware.d.ts.map +1 -1
  103. package/dist/profiler/middleware.js +76 -10
  104. package/dist/profiler/middleware.js.map +1 -1
  105. package/dist/profiler/preview.d.ts +17 -0
  106. package/dist/profiler/preview.d.ts.map +1 -0
  107. package/dist/profiler/preview.js +100 -0
  108. package/dist/profiler/preview.js.map +1 -0
  109. package/dist/profiler/toolbar.d.ts +15 -23
  110. package/dist/profiler/toolbar.d.ts.map +1 -1
  111. package/dist/profiler/toolbar.js +25 -91
  112. package/dist/profiler/toolbar.js.map +1 -1
  113. package/dist/profiler/types.d.ts +6 -1
  114. package/dist/profiler/types.d.ts.map +1 -1
  115. package/dist/router/Router.d.ts +6 -0
  116. package/dist/router/Router.d.ts.map +1 -1
  117. package/dist/router/Router.js +115 -12
  118. package/dist/router/Router.js.map +1 -1
  119. package/dist/rpc/RpcRouter.js +3 -3
  120. package/dist/rpc/RpcRouter.js.map +1 -1
  121. package/dist/scheduler/Scheduler.js +1 -1
  122. package/dist/scheduler/Scheduler.js.map +1 -1
  123. package/dist/security/CookieSigner.d.ts.map +1 -1
  124. package/dist/security/CookieSigner.js +15 -5
  125. package/dist/security/CookieSigner.js.map +1 -1
  126. package/dist/security/SignedUrl.d.ts.map +1 -1
  127. package/dist/security/SignedUrl.js +10 -18
  128. package/dist/security/SignedUrl.js.map +1 -1
  129. package/dist/server/Server.d.ts +4 -1
  130. package/dist/server/Server.d.ts.map +1 -1
  131. package/dist/server/Server.js.map +1 -1
  132. package/dist/server/nativeLimits.d.ts +9 -1
  133. package/dist/server/nativeLimits.d.ts.map +1 -1
  134. package/dist/server/nativeLimits.js +8 -0
  135. package/dist/server/nativeLimits.js.map +1 -1
  136. package/dist/session/Session.d.ts.map +1 -1
  137. package/dist/session/Session.js +13 -4
  138. package/dist/session/Session.js.map +1 -1
  139. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  140. package/dist/session/drivers/MemoryDriver.js +14 -2
  141. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  142. package/dist/storage/StaticMiddleware.d.ts.map +1 -1
  143. package/dist/storage/StaticMiddleware.js +20 -9
  144. package/dist/storage/StaticMiddleware.js.map +1 -1
  145. package/dist/testing/ApiResponse.d.ts.map +1 -1
  146. package/dist/testing/ApiResponse.js +7 -1
  147. package/dist/testing/ApiResponse.js.map +1 -1
  148. package/dist/testing/TestClient.d.ts.map +1 -1
  149. package/dist/testing/TestClient.js +16 -4
  150. package/dist/testing/TestClient.js.map +1 -1
  151. package/dist/utils/Macroable.d.ts +1 -1
  152. package/dist/utils/Macroable.js +2 -2
  153. package/dist/utils/Macroable.js.map +1 -1
  154. package/events.darwin-arm64.node +0 -0
  155. package/events.darwin-x64.node +0 -0
  156. package/events.linux-arm64-gnu.node +0 -0
  157. package/events.linux-x64-gnu.node +0 -0
  158. package/events.win32-x64-msvc.node +0 -0
  159. package/index.darwin-arm64.node +0 -0
  160. package/index.darwin-x64.node +0 -0
  161. package/index.linux-arm64-gnu.node +0 -0
  162. package/index.linux-x64-gnu.node +0 -0
  163. package/index.win32-x64-msvc.node +0 -0
  164. package/package.json +1 -2
  165. package/scheduler.darwin-arm64.node +0 -0
  166. package/scheduler.darwin-x64.node +0 -0
  167. package/scheduler.linux-arm64-gnu.node +0 -0
  168. package/scheduler.linux-x64-gnu.node +0 -0
  169. package/scheduler.win32-x64-msvc.node +0 -0
  170. package/src/Codemods.ts +49 -3
  171. package/src/HttpKernel.ts +18 -7
  172. package/src/Ignitor.ts +21 -4
  173. package/src/bodyparser/BodyParserMiddleware.ts +42 -16
  174. package/src/bodyparser/MultipartFile.ts +46 -67
  175. package/src/bodyparser/parseSize.ts +17 -17
  176. package/src/bodyparser/qsParse.ts +28 -13
  177. package/src/console/prompts.ts +3 -1
  178. package/src/container/Container.ts +97 -81
  179. package/src/events/Emitter.ts +37 -10
  180. package/src/events/helix/helpers.ts +24 -19
  181. package/src/graphql/GraphQLEngine.ts +89 -13
  182. package/src/helpers/MessageBuilder.ts +4 -2
  183. package/src/helpers/errors.ts +16 -0
  184. package/src/helpers/napi-loader.ts +3 -2
  185. package/src/http/HttpContext.ts +20 -4
  186. package/src/http/RedirectBuilder.ts +3 -0
  187. package/src/http/Request.ts +15 -29
  188. package/src/http/Response.ts +15 -7
  189. package/src/http/permissions.ts +31 -15
  190. package/src/index.ts +2 -1
  191. package/src/middleware/Pipeline.ts +1 -1
  192. package/src/native/generated.ts +18 -42
  193. package/src/profiler/Profiler.ts +4 -1
  194. package/src/profiler/client/toolbar.css +595 -0
  195. package/src/profiler/client/toolbar.js +1299 -0
  196. package/src/profiler/client.generated.ts +7 -0
  197. package/src/profiler/collectors.ts +156 -19
  198. package/src/profiler/endpoint.ts +27 -1
  199. package/src/profiler/index.ts +17 -3
  200. package/src/profiler/middleware.ts +74 -9
  201. package/src/profiler/preview.ts +104 -0
  202. package/src/profiler/toolbar.ts +28 -91
  203. package/src/profiler/types.ts +6 -1
  204. package/src/router/Router.ts +136 -13
  205. package/src/rpc/RpcRouter.ts +4 -4
  206. package/src/scheduler/Scheduler.ts +1 -1
  207. package/src/security/CookieSigner.ts +13 -5
  208. package/src/security/SignedUrl.ts +12 -16
  209. package/src/server/Server.ts +4 -1
  210. package/src/server/nativeLimits.ts +18 -1
  211. package/src/session/Session.ts +13 -4
  212. package/src/session/drivers/MemoryDriver.ts +15 -2
  213. package/src/storage/StaticMiddleware.ts +18 -9
  214. package/src/testing/ApiResponse.ts +7 -1
  215. package/src/testing/TestClient.ts +14 -5
  216. package/src/utils/Macroable.ts +2 -2
  217. package/scripts/build-napi-types.mjs +0 -69
  218. package/scripts/copy-napi.mjs +0 -121
  219. package/scripts/generate-mime.mjs +0 -119
  220. package/scripts/generate-napi-types.mjs +0 -187
@@ -288,19 +288,28 @@ export class Session {
288
288
  // The store is untyped by nature, so `T` is the CALLER's claim about what
289
289
  // it put there — one assertion from `unknown`, which is what `T` means
290
290
  // here. Going through `any` first widened nothing and hid the intent.
291
- if (key in this.#data) return this.#data[key] as T
291
+ // OWN keys only: `in` walked the prototype, so an empty session answered
292
+ // `get('toString')` with a function and `has('constructor')` with true.
293
+ if (Object.hasOwn(this.#data, key)) return this.#data[key] as T
292
294
  return defaultValue as T
293
295
  }
294
296
 
295
297
  /** Set a session value. */
296
298
  put(key: string, value: unknown): void {
297
- this.#data[key] = value
299
+ // Defined, not assigned: `put('__proto__', x)` stores a key like any
300
+ // other instead of replacing the session data's prototype.
301
+ Object.defineProperty(this.#data, key, {
302
+ value,
303
+ writable: true,
304
+ enumerable: true,
305
+ configurable: true,
306
+ })
298
307
  this.#dirty = true
299
308
  }
300
309
 
301
310
  /** Check if a key exists. */
302
311
  has(key: string): boolean {
303
- return key in this.#data
312
+ return Object.hasOwn(this.#data, key)
304
313
  }
305
314
 
306
315
  /** Get all session data. */
@@ -504,7 +513,7 @@ export class Session {
504
513
  old<T = unknown>(key: string, defaultValue?: T): T {
505
514
  // Same contract as `get`: `T` is the caller's claim about a store that is
506
515
  // untyped by nature.
507
- if (key in this.#previousFlash) return this.#previousFlash[key] as T
516
+ if (Object.hasOwn(this.#previousFlash, key)) return this.#previousFlash[key] as T
508
517
  return defaultValue as T
509
518
  }
510
519
 
@@ -3,6 +3,16 @@ import type { SessionDriverWithTagging, TaggedSession } from '../Session.js'
3
3
  /** Sweep expired entries once the store reaches this size. */
4
4
  const SWEEP_THRESHOLD = 1000
5
5
 
6
+ /**
7
+ * At most one sweep per this many milliseconds.
8
+ *
9
+ * A sweep walks the whole store. Run on every write past the threshold, a
10
+ * flood of anonymous requests turned each write into a 10 000-entry scan —
11
+ * quadratic, and 20 000 writes took seconds. Between two sweeps the cap below
12
+ * still bounds the store, and a read drops the expired entry it lands on.
13
+ */
14
+ const SWEEP_INTERVAL_MS = 1000
15
+
6
16
  /**
7
17
  * Hard cap on live sessions.
8
18
  *
@@ -25,6 +35,8 @@ export class MemoryDriver implements SessionDriverWithTagging {
25
35
  /** user id → the sessions tagged with it. */
26
36
  #tags: Map<string, Set<string>> = new Map()
27
37
  readonly #maxEntries: number
38
+ /** When the last sweep ran, epoch milliseconds. */
39
+ #lastSweep = 0
28
40
 
29
41
  constructor(options: MemoryDriverOptions = {}) {
30
42
  this.#maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES)
@@ -40,8 +52,9 @@ export class MemoryDriver implements SessionDriverWithTagging {
40
52
  }
41
53
 
42
54
  async write(sessionId: string, data: Record<string, unknown>, ttl: number): Promise<void> {
43
- if (this.#store.size >= SWEEP_THRESHOLD) {
44
- const now = Date.now()
55
+ const now = Date.now()
56
+ if (this.#store.size >= SWEEP_THRESHOLD && now - this.#lastSweep >= SWEEP_INTERVAL_MS) {
57
+ this.#lastSweep = now
45
58
  for (const [id, entry] of this.#store) {
46
59
  if (entry.expiresAt < now) this.#store.delete(id)
47
60
  }
@@ -147,7 +147,11 @@ export class StaticMiddleware {
147
147
  const method = ctx.request.method()
148
148
  if (method !== 'GET' && method !== 'HEAD') return next()
149
149
 
150
- const reqPath = ctx.request.path()
150
+ // Outside the prefix the request is none of this middleware's business —
151
+ // not even a dot-file refusal, or `{ prefix: '/static', dotFiles: 'deny' }`
152
+ // answers 403 to an application route such as `/.well-known/...`.
153
+ const reqPath = this.#withinPrefix(ctx.request.path())
154
+ if (reqPath === null) return next()
151
155
 
152
156
  // A dotfile under `deny` is answered, not passed on: falling through would
153
157
  // leak its existence through whatever the router answers next. The check
@@ -327,17 +331,22 @@ export class StaticMiddleware {
327
331
  * from the descriptor: what gets served is then the same file that passed the
328
332
  * checks, which a path re-read after the fact cannot promise.
329
333
  */
334
+ /**
335
+ * The request path below the prefix, or `null` when it lies outside it.
336
+ * Match only when the prefix is followed by `/` or end-of-path — otherwise
337
+ * `/staticx/foo.js` would be intercepted when the configured prefix is
338
+ * `/static`, hijacking unrelated routes.
339
+ */
340
+ #withinPrefix(reqPath: string): string | null {
341
+ if (this.#prefix === '') return reqPath
342
+ if (!(reqPath === this.#prefix || reqPath.startsWith(`${this.#prefix}/`))) return null
343
+ return reqPath.slice(this.#prefix.length) || '/'
344
+ }
345
+
346
+ /** `reqPath` is already relative to the prefix ({@link #withinPrefix}). */
330
347
  async #openServableFile(
331
348
  reqPath: string,
332
349
  ): Promise<{ handle: FileHandle; filePath: string; stat: fs.Stats } | null> {
333
- if (this.#prefix !== '') {
334
- // Match only when the prefix is followed by `/` or end-of-path —
335
- // otherwise `/staticx/foo.js` would be intercepted when the configured
336
- // prefix is `/static`, hijacking unrelated routes.
337
- if (!(reqPath === this.#prefix || reqPath.startsWith(`${this.#prefix}/`))) return null
338
- reqPath = reqPath.slice(this.#prefix.length) || '/'
339
- }
340
-
341
350
  // Percent-decoding happens BEFORE any check and BEFORE resolution on
342
351
  // purpose: the dot-file policy has to see the name that will be opened
343
352
  // (`%2eenv` is `.env`), and `%2e%2e%2f` is `../`, which has to be
@@ -78,7 +78,13 @@ export function partialMatch(actual: unknown, expected: unknown): boolean {
78
78
 
79
79
  /** Split a `Set-Cookie` header value into its individual cookie strings. */
80
80
  function splitSetCookie(setCookie: string): string[] {
81
- return setCookie.split(/,(?=\s*[\w-]+=)/).map((c) => c.trim())
81
+ // One cookie per line, as the response parser stores them; a header folded
82
+ // with commas by another source is still split where a new `name=` starts.
83
+ return setCookie
84
+ .split('\n')
85
+ .flatMap((line) => line.split(/,(?=\s*[\w-]+=)/))
86
+ .map((c) => c.trim())
87
+ .filter((c) => c !== '')
82
88
  }
83
89
 
84
90
  /** A parsed response cookie — `name` + `value` plus any attributes present. */
@@ -195,7 +195,7 @@ export class TestClient {
195
195
  m,
196
196
  p,
197
197
  { ...this.#headers, ...init.headers },
198
- init.body.toString('utf8'),
198
+ init.body,
199
199
  init.timeoutMs,
200
200
  )
201
201
  }
@@ -288,7 +288,7 @@ async function sendRequest(
288
288
  method: string,
289
289
  path: string,
290
290
  headers: Dict,
291
- body: string,
291
+ body: Buffer,
292
292
  timeoutMs?: number,
293
293
  ): Promise<TestResponse> {
294
294
  const { connect } = await import('node:net')
@@ -300,7 +300,10 @@ async function sendRequest(
300
300
  .map(([k, v]) => `${k}: ${v}`)
301
301
  .join('\r\n')
302
302
 
303
- const contentLength = Buffer.byteLength(body, 'utf8')
303
+ // The body stays a Buffer end to end: a multipart upload of a PNG or a
304
+ // PDF is not valid UTF-8, and a round trip through a string replaced
305
+ // every such byte with U+FFFD before the server ever saw it.
306
+ const contentLength = body.length
304
307
 
305
308
  let request = `${method} ${path} HTTP/1.1\r\n`
306
309
  request += `Host: localhost:${port}\r\n`
@@ -312,9 +315,9 @@ async function sendRequest(
312
315
  request += `${headerLines}\r\n`
313
316
  }
314
317
  request += '\r\n'
315
- request += body
316
318
 
317
319
  socket.write(request)
320
+ if (contentLength > 0) socket.write(body)
318
321
  })
319
322
 
320
323
  const chunks: Buffer[] = []
@@ -371,13 +374,19 @@ export function parseHttpResponse(raw: string | Buffer): TestResponse {
371
374
  const [, statusDigits] = statusLine.match(/HTTP\/\d+\.\d+ (\d+)/) ?? []
372
375
  const status = statusDigits === undefined ? 0 : Number.parseInt(statusDigits, 10)
373
376
 
377
+ // A repeated header is never overwritten. Set-Cookie lines are joined with
378
+ // `\n`, the way `Response.getHeaders()` hands them to the serializer: a
379
+ // cookie's `Expires` holds a comma, so a comma cannot separate two of them
380
+ // (RFC 6265 §3). Any other field is combined with `, ` (RFC 9110 §5.3).
374
381
  const headers: Dict = {}
375
382
  for (const line of lines.slice(1)) {
376
383
  const colonIdx = line.indexOf(':')
377
384
  if (colonIdx === -1) continue
378
385
  const key = line.slice(0, colonIdx).trim().toLowerCase()
379
386
  const value = line.slice(colonIdx + 1).trim()
380
- headers[key] = value
387
+ const existing = headers[key]
388
+ if (existing === undefined) headers[key] = value
389
+ else headers[key] = `${existing}${key === 'set-cookie' ? '\n' : ', '}${value}`
381
390
  }
382
391
 
383
392
  // RFC 7230 §4.1: when Transfer-Encoding includes `chunked`, the message body
@@ -27,12 +27,12 @@ export type MacroFn = (...args: unknown[]) => unknown
27
27
  /** A getter implementation — declare `this` in your callback to type the receiver. */
28
28
  export type GetterFn = () => unknown
29
29
 
30
- // biome-ignore lint/complexity/noStaticOnlyClass: eight classes extend this one to inherit macro()/getter(); it is a base class, not a namespace.
30
+ // biome-ignore lint/complexity/noStaticOnlyClass: nine classes extend this one to inherit macro()/getter(); it is a base class, not a namespace.
31
31
  export class Macroable {
32
32
  /**
33
33
  * Attach a method to every instance of THIS class (AdonisJS `macro`).
34
34
  *
35
- * `this.prototype`, not `Macroable.prototype`: eight classes extend this one,
35
+ * `this.prototype`, not `Macroable.prototype`: nine classes extend this one,
36
36
  * and defining on the base put every macro on all of them — a
37
37
  * `Response.macro('json', …)` also landed on `Request`, `Router` and
38
38
  * `HttpContext`, and two subclasses declaring the same name clobbered each
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Regenerate `src/native/generated.ts` from the Rust.
4
- *
5
- * Node rather than a shell script because this runs inside `build:napi`, and
6
- * that runs on the Windows prebuild runner too: a bash `mktemp` there hands
7
- * cargo a `/tmp/...` path the native proc-macro cannot write to, so the
8
- * type-def file comes back empty and the build fails for no visible reason.
9
- *
10
- * One crate at a time on purpose: with napi-derive 2 the writes all went to a
11
- * single `TYPE_DEF_TMP_PATH` and a parallel build interleaved them
12
- * while cargo compiles, and a parallel build interleaves the writes —
13
- * definitions go missing, silently, and the generated file comes out short.
14
- */
15
-
16
- import { execFileSync } from 'node:child_process'
17
- import { createRequire } from 'node:module'
18
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
19
- import { tmpdir } from 'node:os'
20
- import { dirname, join } from 'node:path'
21
- import { fileURLToPath } from 'node:url'
22
-
23
- const CRATES = ['ream-events-napi', 'ream-http-napi', 'ream-scheduler-napi']
24
-
25
- const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
26
- const output = join('src', 'native', 'generated.ts')
27
- const scratch = mkdtempSync(join(tmpdir(), 'napi-types-'))
28
-
29
- try {
30
- const lines = []
31
- for (const crate of CRATES) {
32
- const perCrate = join(scratch, crate)
33
- writeFileSync(perCrate, '')
34
- execFileSync('cargo', ['build', '-p', crate], {
35
- cwd: packageRoot,
36
- env: { ...process.env, NAPI_TYPE_DEF_TMP_FOLDER: scratch },
37
- stdio: ['ignore', 'ignore', 'inherit'],
38
- })
39
- const emitted = readFileSync(perCrate, 'utf8').split('\n').filter(Boolean)
40
- if (emitted.length === 0) {
41
- throw new Error(
42
- `[napi-types] ${crate} emitted nothing — is napi-derive's "type-def" feature on?`,
43
- )
44
- }
45
- lines.push(...emitted)
46
- }
47
-
48
- const combined = join(scratch, 'combined.jsonl')
49
- writeFileSync(combined, `${lines.join('\n')}\n`)
50
-
51
- execFileSync(
52
- process.execPath,
53
- [join('scripts', 'generate-napi-types.mjs'), combined, output],
54
- { cwd: packageRoot, stdio: 'inherit' },
55
- )
56
-
57
- // Formatted here rather than excluded from the linter: the file is checked
58
- // in, so it should read like the rest of the tree — and formatting it at
59
- // generation means it never shows up as a diff someone has to fix by hand.
60
- // Resolved through the package entry rather than `node_modules/.bin`, whose
61
- // shim is a shell script the Windows runner cannot execute directly.
62
- const biome = createRequire(import.meta.url).resolve('@biomejs/biome/bin/biome')
63
- execFileSync(process.execPath, [biome, 'format', '--write', output], {
64
- cwd: packageRoot,
65
- stdio: ['ignore', 'ignore', 'inherit'],
66
- })
67
- } finally {
68
- rmSync(scratch, { recursive: true, force: true })
69
- }
@@ -1,121 +0,0 @@
1
- // Copy every freshly-built Ream NAPI cdylib out of cargo's target
2
- // directory and into the package root with the platform-suffix naming
3
- // convention used by `napi-loader.ts`:
4
- //
5
- // packages/ream/index.linux-x64-gnu.node (ream-http-napi)
6
- // packages/ream/scheduler.linux-x64-gnu.node (ream-scheduler-napi)
7
- // packages/ream/index.darwin-arm64.node
8
- // ...
9
- //
10
- // The TS loader walks two upper directories from each `loadNapi` caller
11
- // to find these files, so the binaries MUST live at the package root
12
- // (not under target/) to be discovered at runtime.
13
- //
14
- // Usage:
15
- //
16
- // pnpm --filter @c9up/ream build:rust # cargo build --release
17
- // pnpm --filter @c9up/ream build:napi # build:rust + this script
18
- //
19
- // The package.json script chains both steps. CI / npm publish runs
20
- // `build:napi` once per platform during the matrix build — every
21
- // crate listed in `NAPI_CRATES` ships in the tarball.
22
-
23
- import { copyFileSync, existsSync } from 'node:fs'
24
- import { dirname, join, resolve } from 'node:path'
25
- import { arch, env, platform } from 'node:process'
26
- import { fileURLToPath } from 'node:url'
27
-
28
- const here = dirname(fileURLToPath(import.meta.url))
29
- const root = join(here, '..')
30
-
31
- // Rust target triple -> { suffix, os }. Set CARGO_BUILD_TARGET to cross-compile
32
- // (e.g. build the x86_64-apple-darwin binary on an arm64 macOS runner so we
33
- // don't depend on the scarce macos-13 Intel runners). When unset we fall back
34
- // to the host platform/arch and the default target/release dir.
35
- const TRIPLE_MAP = {
36
- 'x86_64-unknown-linux-gnu': { suffix: 'linux-x64-gnu', os: 'linux' },
37
- 'aarch64-unknown-linux-gnu': { suffix: 'linux-arm64-gnu', os: 'linux' },
38
- 'x86_64-apple-darwin': { suffix: 'darwin-x64', os: 'darwin' },
39
- 'aarch64-apple-darwin': { suffix: 'darwin-arm64', os: 'darwin' },
40
- 'x86_64-pc-windows-msvc': { suffix: 'win32-x64-msvc', os: 'win32' },
41
- }
42
-
43
- const HOST_SUFFIX_MAP = {
44
- 'linux-x64': 'linux-x64-gnu',
45
- 'linux-arm64': 'linux-arm64-gnu',
46
- 'darwin-x64': 'darwin-x64',
47
- 'darwin-arm64': 'darwin-arm64',
48
- 'win32-x64': 'win32-x64-msvc',
49
- }
50
-
51
- // Cargo writes its artifacts under CARGO_TARGET_DIR when that is set — a
52
- // shared cache, a CI mount — so they are not under this package's `target/` at
53
- // all. A relative value is resolved against the directory cargo ran in, which
54
- // is this package root.
55
- const targetDir = env.CARGO_TARGET_DIR
56
- ? resolve(root, env.CARGO_TARGET_DIR)
57
- : join(root, 'target')
58
-
59
- const triple = env.CARGO_BUILD_TARGET ?? ''
60
- let suffix
61
- let os
62
- let releaseDir
63
- if (triple) {
64
- const entry = TRIPLE_MAP[triple]
65
- if (!entry) {
66
- throw new Error(`[ream:napi] unsupported CARGO_BUILD_TARGET: ${triple}`)
67
- }
68
- suffix = entry.suffix
69
- os = entry.os
70
- releaseDir = join(targetDir, triple, 'release')
71
- } else {
72
- suffix = HOST_SUFFIX_MAP[`${platform}-${arch}`]
73
- os = platform
74
- releaseDir = join(targetDir, 'release')
75
- if (!suffix) {
76
- throw new Error(`[ream:napi] unsupported platform/arch: ${platform}-${arch}`)
77
- }
78
- }
79
-
80
- // Every NAPI crate shipped from the @c9up/ream package. `crateLib` is the
81
- // underscore-suffixed name cargo produces under `target/release/`;
82
- // `targetName` is what `loadNapi({ binaryName })` expects at the package
83
- // root (see `napi-loader.ts` — the binary lookup prefix matches the
84
- // `binaryName` option passed by each TS consumer).
85
- const NAPI_CRATES = [
86
- { crateLib: 'ream_http_napi', targetName: 'index' },
87
- { crateLib: 'ream_scheduler_napi', targetName: 'scheduler' },
88
- { crateLib: 'ream_events_napi', targetName: 'events' },
89
- ]
90
-
91
- // `crate-type = ["cdylib"]` settles on the platform-conventional file
92
- // name. Linux + macOS prepend `lib`; Windows drops the prefix and uses
93
- // `.dll`. Check both Windows shapes so we degrade gracefully if upstream
94
- // cargo ever switches naming.
95
- function buildCandidates(crateLib) {
96
- if (os === 'win32') {
97
- return [
98
- join(releaseDir, `${crateLib}.dll`),
99
- join(releaseDir, `lib${crateLib}.dll`),
100
- ]
101
- }
102
- if (os === 'darwin') {
103
- return [join(releaseDir, `lib${crateLib}.dylib`)]
104
- }
105
- return [join(releaseDir, `lib${crateLib}.so`)]
106
- }
107
-
108
- for (const { crateLib, targetName } of NAPI_CRATES) {
109
- const candidates = buildCandidates(crateLib)
110
- const source = candidates.find((candidate) => existsSync(candidate))
111
- if (!source) {
112
- throw new Error(
113
- `[ream:napi] native library for '${crateLib}' not found. Looked for:\n${candidates
114
- .map((p) => `- ${p}`)
115
- .join('\n')}\nRun 'pnpm --filter @c9up/ream build:rust' first.`,
116
- )
117
- }
118
- const target = join(root, `${targetName}.${suffix}.node`)
119
- copyFileSync(source, target)
120
- console.log(`[ream:napi] copied ${source} -> ${target}`)
121
- }
@@ -1,119 +0,0 @@
1
- /**
2
- * Regenerate `src/http/mime.ts` from mime-db.
3
- *
4
- * The table is data, not logic, and hand-maintaining it is how `.yaml` and
5
- * `.sql` went missing. `mime-types` reads the same two fields out of mime-db —
6
- * the resolved extension map and the per-type `charset` — so generating from
7
- * there keeps `contentType()` answering exactly what the package answered.
8
- *
9
- * mime-db and mime-types are dev-only: they are read HERE, at generation time,
10
- * and never imported by the framework.
11
- *
12
- * node scripts/generate-mime.mjs
13
- */
14
-
15
- import { createRequire } from 'node:module'
16
- import { writeFile } from 'node:fs/promises'
17
- import { fileURLToPath } from 'node:url'
18
- import { dirname, join } from 'node:path'
19
-
20
- const require = createRequire(import.meta.url)
21
- const mimeTypes = require('mime-types')
22
- const db = require('mime-db')
23
- const dbVersion = require('mime-db/package.json').version
24
-
25
- const here = dirname(fileURLToPath(import.meta.url))
26
- const target = join(here, '..', 'src', 'http', 'mime.ts')
27
-
28
- /**
29
- * Emitted in the package's own style — single quotes, no semicolons — so a
30
- * regenerated file is already what `biome check` wants and the diff after a
31
- * regeneration is the data that changed, nothing else.
32
- */
33
- const quote = (value) => `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
34
- /** An identifier when it is one, a quoted key otherwise (`7z`, `3gp`…). */
35
- const key = (ext) => (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(ext) ? ext : quote(ext))
36
-
37
- const extensions = Object.keys(mimeTypes.types).sort()
38
- const rows = extensions.map((e) => ` ${key(e)}: ${quote(mimeTypes.types[e])},`).join('\n')
39
-
40
- const charsets = Object.entries(db)
41
- .filter(([, entry]) => entry.charset)
42
- .sort(([a], [b]) => a.localeCompare(b))
43
- .map(([type, entry]) => ` ${quote(type)}: ${quote(entry.charset.toLowerCase())},`)
44
- .join('\n')
45
-
46
- await writeFile(target, render({ rows, charsets, dbVersion, count: extensions.length }))
47
- process.stdout.write(`mime.ts: ${extensions.length} extensions from mime-db ${dbVersion}\n`)
48
-
49
- function render({ rows, charsets, dbVersion, count }) {
50
- return `/**
51
- * Content types for a response, resolved here rather than pulled in.
52
- *
53
- * \`response.type("txt")\` has to become \`text/plain; charset=utf-8\`, which is a
54
- * lookup table and one charset rule — the same job \`@c9up/archive\` already
55
- * does in its own table, so the framework was carrying a dependency for work
56
- * the ecosystem already owned.
57
- *
58
- * A hand-written table is what made that a regression rather than a saving:
59
- * it knew the extensions someone thought of, so \`.yaml\`, \`.sql\`, \`.opus\` and
60
- * \`.xhtml\` fell through and the caller's own string went out as the content
61
- * type. The tables below are GENERATED from mime-db instead, which is where
62
- * \`mime-types\` reads them too, so the resolution is the same one and stays
63
- * the same one.
64
- *
65
- * Regenerate with:
66
- *
67
- * node scripts/generate-mime.mjs
68
- *
69
- * Do not edit {@link TYPES} or {@link CHARSETS} by hand.
70
- *
71
- * @generated from mime-db ${dbVersion} (${count} extensions)
72
- */
73
-
74
- /** Extension → type. No leading dot; lowercase. */
75
- const TYPES: Record<string, string> = {
76
- ${rows}
77
- }
78
-
79
- /**
80
- * Types that declare a charset of their own, lowercased.
81
- *
82
- * Everything else under \`text/\` defaults to utf-8; everything else has none.
83
- * That is mime-db's \`charset\` field plus \`mime-types\`' \`text/*\` fallback.
84
- */
85
- const CHARSETS: Record<string, string> = {
86
- ${charsets}
87
- }
88
-
89
- /** The charset for a type, or \`undefined\` when it carries none. */
90
- function charsetFor(type: string): string | undefined {
91
- const base = type.split(';')[0].trim().toLowerCase()
92
- const declared = CHARSETS[base]
93
- if (declared !== undefined) return declared
94
- return base.startsWith('text/') ? 'utf-8' : undefined
95
- }
96
-
97
- /** The type for a bare extension, or \`undefined\` when it is not one we know. */
98
- export function lookupType(extension: string): string | undefined {
99
- const ext = extension.replace(/^.*\\./, '').toLowerCase()
100
- return TYPES[ext]
101
- }
102
-
103
- /**
104
- * A full \`Content-Type\` for an extension or a type (\`mime-types.contentType\`).
105
- *
106
- * Returns \`false\` for something it cannot resolve, as the package did, so the
107
- * caller keeps its existing fallback rather than writing \`content-type: false\`.
108
- */
109
- export function contentType(input: string): string | false {
110
- if (input.length === 0) return false
111
- // Already a type — possibly with parameters the caller set itself.
112
- const type = input.includes('/') ? input : lookupType(input)
113
- if (type === undefined) return false
114
- if (type.includes('charset=')) return type
115
- const charset = charsetFor(type)
116
- return charset === undefined ? type : \`\${type}; charset=\${charset}\`
117
- }
118
- `
119
- }