@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.
- package/README.md +5 -6
- package/dist/Codemods.d.ts +8 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +29 -1
- package/dist/Codemods.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +17 -5
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +5 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +12 -1
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +38 -11
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +21 -15
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +40 -58
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/parseSize.d.ts +7 -4
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +12 -17
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +10 -0
- package/dist/bodyparser/qsParse.d.ts.map +1 -1
- package/dist/bodyparser/qsParse.js +27 -12
- package/dist/bodyparser/qsParse.js.map +1 -1
- package/dist/console/prompts.d.ts.map +1 -1
- package/dist/console/prompts.js +3 -1
- package/dist/console/prompts.js.map +1 -1
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +61 -71
- package/dist/container/Container.js.map +1 -1
- package/dist/events/Emitter.d.ts +12 -0
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +36 -9
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/helix/helpers.d.ts.map +1 -1
- package/dist/events/helix/helpers.js +19 -9
- package/dist/events/helix/helpers.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +8 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +77 -11
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/MessageBuilder.d.ts +4 -2
- package/dist/helpers/MessageBuilder.d.ts.map +1 -1
- package/dist/helpers/MessageBuilder.js +4 -2
- package/dist/helpers/MessageBuilder.js.map +1 -1
- package/dist/helpers/errors.d.ts +11 -0
- package/dist/helpers/errors.d.ts.map +1 -0
- package/dist/helpers/errors.js +17 -0
- package/dist/helpers/errors.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts.map +1 -1
- package/dist/helpers/napi-loader.js +3 -2
- package/dist/helpers/napi-loader.js.map +1 -1
- package/dist/http/HttpContext.d.ts +20 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +10 -4
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +4 -0
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +15 -29
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +7 -4
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +14 -6
- package/dist/http/Response.js.map +1 -1
- package/dist/http/permissions.d.ts +14 -8
- package/dist/http/permissions.d.ts.map +1 -1
- package/dist/http/permissions.js +26 -16
- package/dist/http/permissions.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.js +1 -1
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +16 -30
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/profiler/Profiler.d.ts.map +1 -1
- package/dist/profiler/Profiler.js +4 -1
- package/dist/profiler/Profiler.js.map +1 -1
- package/dist/profiler/client.generated.d.ts +5 -0
- package/dist/profiler/client.generated.d.ts.map +1 -0
- package/dist/profiler/client.generated.js +6 -0
- package/dist/profiler/client.generated.js.map +1 -0
- package/dist/profiler/collectors.d.ts +92 -13
- package/dist/profiler/collectors.d.ts.map +1 -1
- package/dist/profiler/collectors.js +106 -19
- package/dist/profiler/collectors.js.map +1 -1
- package/dist/profiler/endpoint.d.ts +2 -1
- package/dist/profiler/endpoint.d.ts.map +1 -1
- package/dist/profiler/endpoint.js +25 -1
- package/dist/profiler/endpoint.js.map +1 -1
- package/dist/profiler/index.d.ts +8 -4
- package/dist/profiler/index.d.ts.map +1 -1
- package/dist/profiler/index.js +11 -4
- package/dist/profiler/index.js.map +1 -1
- package/dist/profiler/middleware.d.ts.map +1 -1
- package/dist/profiler/middleware.js +76 -10
- package/dist/profiler/middleware.js.map +1 -1
- package/dist/profiler/preview.d.ts +17 -0
- package/dist/profiler/preview.d.ts.map +1 -0
- package/dist/profiler/preview.js +100 -0
- package/dist/profiler/preview.js.map +1 -0
- package/dist/profiler/toolbar.d.ts +15 -23
- package/dist/profiler/toolbar.d.ts.map +1 -1
- package/dist/profiler/toolbar.js +25 -91
- package/dist/profiler/toolbar.js.map +1 -1
- package/dist/profiler/types.d.ts +6 -1
- package/dist/profiler/types.d.ts.map +1 -1
- package/dist/router/Router.d.ts +6 -0
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +115 -12
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcRouter.js +3 -3
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Scheduler.js +1 -1
- package/dist/scheduler/Scheduler.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +15 -5
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.d.ts.map +1 -1
- package/dist/security/SignedUrl.js +10 -18
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/server/Server.d.ts +4 -1
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js.map +1 -1
- package/dist/server/nativeLimits.d.ts +9 -1
- package/dist/server/nativeLimits.d.ts.map +1 -1
- package/dist/server/nativeLimits.js +8 -0
- package/dist/server/nativeLimits.js.map +1 -1
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +13 -4
- package/dist/session/Session.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +14 -2
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +20 -9
- package/dist/storage/StaticMiddleware.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +7 -1
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +16 -4
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +1 -1
- package/dist/utils/Macroable.js +2 -2
- package/dist/utils/Macroable.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -2
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Codemods.ts +49 -3
- package/src/HttpKernel.ts +18 -7
- package/src/Ignitor.ts +21 -4
- package/src/bodyparser/BodyParserMiddleware.ts +42 -16
- package/src/bodyparser/MultipartFile.ts +46 -67
- package/src/bodyparser/parseSize.ts +17 -17
- package/src/bodyparser/qsParse.ts +28 -13
- package/src/console/prompts.ts +3 -1
- package/src/container/Container.ts +97 -81
- package/src/events/Emitter.ts +37 -10
- package/src/events/helix/helpers.ts +24 -19
- package/src/graphql/GraphQLEngine.ts +89 -13
- package/src/helpers/MessageBuilder.ts +4 -2
- package/src/helpers/errors.ts +16 -0
- package/src/helpers/napi-loader.ts +3 -2
- package/src/http/HttpContext.ts +20 -4
- package/src/http/RedirectBuilder.ts +3 -0
- package/src/http/Request.ts +15 -29
- package/src/http/Response.ts +15 -7
- package/src/http/permissions.ts +31 -15
- package/src/index.ts +2 -1
- package/src/middleware/Pipeline.ts +1 -1
- package/src/native/generated.ts +18 -42
- package/src/profiler/Profiler.ts +4 -1
- package/src/profiler/client/toolbar.css +595 -0
- package/src/profiler/client/toolbar.js +1299 -0
- package/src/profiler/client.generated.ts +7 -0
- package/src/profiler/collectors.ts +156 -19
- package/src/profiler/endpoint.ts +27 -1
- package/src/profiler/index.ts +17 -3
- package/src/profiler/middleware.ts +74 -9
- package/src/profiler/preview.ts +104 -0
- package/src/profiler/toolbar.ts +28 -91
- package/src/profiler/types.ts +6 -1
- package/src/router/Router.ts +136 -13
- package/src/rpc/RpcRouter.ts +4 -4
- package/src/scheduler/Scheduler.ts +1 -1
- package/src/security/CookieSigner.ts +13 -5
- package/src/security/SignedUrl.ts +12 -16
- package/src/server/Server.ts +4 -1
- package/src/server/nativeLimits.ts +18 -1
- package/src/session/Session.ts +13 -4
- package/src/session/drivers/MemoryDriver.ts +15 -2
- package/src/storage/StaticMiddleware.ts +18 -9
- package/src/testing/ApiResponse.ts +7 -1
- package/src/testing/TestClient.ts +14 -5
- package/src/utils/Macroable.ts +2 -2
- package/scripts/build-napi-types.mjs +0 -69
- package/scripts/copy-napi.mjs +0 -121
- package/scripts/generate-mime.mjs +0 -119
- package/scripts/generate-napi-types.mjs +0 -187
package/src/session/Session.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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]
|
|
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
|
package/src/utils/Macroable.ts
CHANGED
|
@@ -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:
|
|
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`:
|
|
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
|
-
}
|
package/scripts/copy-napi.mjs
DELETED
|
@@ -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
|
-
}
|