@mpen/routekit 0.1.0 → 0.1.2
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/dist/bin.d.mts +4 -0
- package/dist/client/react.d.mts +178 -0
- package/dist/client/react.mjs +142 -0
- package/dist/client.d.mts +433 -0
- package/dist/client.mjs +264 -0
- package/dist/content-BuDOmhH_.mjs +102 -0
- package/dist/core-CzUCxvGk.d.mts +140 -0
- package/dist/core-DbmQauwS.mjs +81 -0
- package/dist/handlers.d.mts +72 -0
- package/dist/handlers.mjs +153 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1152 -0
- package/dist/middleware.d.mts +388 -0
- package/dist/middleware.mjs +1222 -0
- package/dist/request-Dn0zc-xm.mjs +1025 -0
- package/dist/response/content.d.mts +79 -0
- package/dist/response/content.mjs +2 -0
- package/dist/response/json-rpc.d.mts +1 -0
- package/dist/response/json-rpc.mjs +1 -0
- package/dist/response/problem/valibot.d.mts +230 -0
- package/dist/response/problem/valibot.mjs +258 -0
- package/dist/response/problem.d.mts +415 -0
- package/dist/response/problem.mjs +183 -0
- package/dist/response/status.d.mts +45 -0
- package/dist/response/status.mjs +2 -0
- package/dist/responses-B379Ep9Y.d.mts +296 -0
- package/dist/responses-BpVrgeYi.mjs +101 -0
- package/dist/router-Cwb7ak0J.d.mts +1819 -0
- package/dist/routes.d.mts +282 -0
- package/dist/routes.mjs +311 -0
- package/dist/status-C-8mw-FB.mjs +59 -0
- package/dist/valibot-D7liFYyB.d.mts +290 -0
- package/dist/valibot-Du97X-TS.mjs +326 -0
- package/package.json +8 -2
- package/src/bin/gen-api-client.test.ts +0 -70
- package/src/bin/gen-api-client.ts +0 -986
- package/src/client/headers.ts +0 -31
- package/src/client/index.ts +0 -8
- package/src/client/promise.ts +0 -11
- package/src/client/react/index.test.tsx +0 -266
- package/src/client/react/index.ts +0 -431
- package/src/client/responses.test.ts +0 -151
- package/src/client/responses.ts +0 -278
- package/src/client/transport.ts +0 -74
- package/src/client/transports/body-codec.ts +0 -61
- package/src/client/transports/fetch.ts +0 -113
- package/src/client/tsconfig.json +0 -9
- package/src/client/types.ts +0 -15
- package/src/client/url.ts +0 -31
- package/src/index.ts +0 -63
- package/src/router/fetch-types.ts +0 -13
- package/src/router/handlers/index.ts +0 -2
- package/src/router/handlers/openapi/index.ts +0 -2
- package/src/router/handlers/openapi/openapi.ts +0 -293
- package/src/router/integration/zod-openapi.test.ts +0 -74
- package/src/router/lib/charset.test.ts +0 -22
- package/src/router/lib/charset.ts +0 -133
- package/src/router/lib/collections.ts +0 -3
- package/src/router/lib/format.test.ts +0 -67
- package/src/router/lib/format.ts +0 -35
- package/src/router/lib/host.ts +0 -4
- package/src/router/lib/json-schema.ts +0 -6
- package/src/router/lib/media-type.test.ts +0 -122
- package/src/router/lib/media-type.ts +0 -289
- package/src/router/lib/pathname.test.ts +0 -18
- package/src/router/lib/pathname.ts +0 -19
- package/src/router/lib/route-names.ts +0 -70
- package/src/router/lib/route-normalize.test.ts +0 -36
- package/src/router/lib/route-normalize.ts +0 -67
- package/src/router/lib/schema-merge.ts +0 -56
- package/src/router/middleware/accept-ctx.test.ts +0 -33
- package/src/router/middleware/accept-ctx.ts +0 -12
- package/src/router/middleware/body-limit.test.ts +0 -112
- package/src/router/middleware/body-limit.ts +0 -121
- package/src/router/middleware/content-type-context.ts +0 -0
- package/src/router/middleware/cors.test.ts +0 -269
- package/src/router/middleware/cors.ts +0 -490
- package/src/router/middleware/csrf.test.ts +0 -106
- package/src/router/middleware/csrf.ts +0 -192
- package/src/router/middleware/define.ts +0 -249
- package/src/router/middleware/index.ts +0 -34
- package/src/router/middleware/jsxhtml-response.ts +0 -0
- package/src/router/middleware/oas-swagger.ts +0 -0
- package/src/router/middleware/rate-limit.test.ts +0 -886
- package/src/router/middleware/rate-limit.ts +0 -920
- package/src/router/middleware/request-id-ctx.test.ts +0 -183
- package/src/router/middleware/request-id-ctx.ts +0 -135
- package/src/router/middleware/request-logger-format.test.ts +0 -16
- package/src/router/middleware/request-logger-format.ts +0 -269
- package/src/router/middleware/request-logger.test.ts +0 -267
- package/src/router/middleware/request-logger.ts +0 -131
- package/src/router/middleware/start-time-ctx.ts +0 -5
- package/src/router/request.ts +0 -611
- package/src/router/response/core.ts +0 -181
- package/src/router/response/directives.ts +0 -233
- package/src/router/response/formats/content/bodyless.ts +0 -54
- package/src/router/response/formats/content/content.ts +0 -79
- package/src/router/response/formats/content/index.ts +0 -2
- package/src/router/response/formats/json-rpc/index.ts +0 -2
- package/src/router/response/formats/problem/badRequest.ts +0 -90
- package/src/router/response/formats/problem/conflict.ts +0 -90
- package/src/router/response/formats/problem/created.ts +0 -40
- package/src/router/response/formats/problem/index.ts +0 -27
- package/src/router/response/formats/problem/notFound.ts +0 -90
- package/src/router/response/formats/problem/permissionDenied.ts +0 -90
- package/src/router/response/formats/problem/problem.test.ts +0 -888
- package/src/router/response/formats/problem/rateLimited.ts +0 -90
- package/src/router/response/formats/problem/responses.ts +0 -219
- package/src/router/response/formats/problem/root-errors.ts +0 -48
- package/src/router/response/formats/problem/sessionExpired.ts +0 -90
- package/src/router/response/formats/problem/types.ts +0 -170
- package/src/router/response/formats/problem/unauthenticated.ts +0 -90
- package/src/router/response/formats/problem/valibot.ts +0 -410
- package/src/router/response/formats/status/index.ts +0 -1
- package/src/router/response/formats/status/responses.ts +0 -59
- package/src/router/response/formats/status/status.test.ts +0 -21
- package/src/router/response/framers.ts +0 -85
- package/src/router/response/index.ts +0 -28
- package/src/router/response/openapi.test.ts +0 -96
- package/src/router/response/openapi.ts +0 -1
- package/src/router/response/serializers.ts +0 -66
- package/src/router/response/stream.ts +0 -35
- package/src/router/router.test.ts +0 -1571
- package/src/router/router.ts +0 -1965
- package/src/router/routes/index.ts +0 -46
- package/src/router/routes/valibot/index.ts +0 -18
- package/src/router/routes/valibot/valibot.ts +0 -1393
- package/src/router/routes/valibot.test.ts +0 -286
- package/src/router/routes/zod/index.ts +0 -18
- package/src/router/routes/zod/zod.ts +0 -1318
- package/src/router/routes/zod.test.ts +0 -280
- package/src/router/server-interface.ts +0 -31
- package/src/router/types.ts +0 -657
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it } from 'bun:test'
|
|
3
|
-
import { HttpMethod } from '@mpen/http'
|
|
4
|
-
import { Router } from '../router'
|
|
5
|
-
import { requestIdCtx } from './request-id-ctx'
|
|
6
|
-
import type { RouterHeadersInit } from '../fetch-types'
|
|
7
|
-
|
|
8
|
-
function makeRequest(headers?: RouterHeadersInit): Request {
|
|
9
|
-
const init: RequestInit = { method: HttpMethod.GET }
|
|
10
|
-
if (headers) init.headers = headers
|
|
11
|
-
return new Request('https://example.com/', init)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe(requestIdCtx.name, () => {
|
|
15
|
-
it('uses the request id header when present', async () => {
|
|
16
|
-
const router = new Router()
|
|
17
|
-
router.useRequest(requestIdCtx())
|
|
18
|
-
router.add({
|
|
19
|
-
method: HttpMethod.GET,
|
|
20
|
-
path: '/',
|
|
21
|
-
handler: ({ requestId }) => {
|
|
22
|
-
expect(requestId).toBe('req-123')
|
|
23
|
-
return new Response(requestId)
|
|
24
|
-
},
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const response = await router.fetch(makeRequest({ 'x-request-id': 'req-123' }))
|
|
28
|
-
|
|
29
|
-
expect(await response.text()).toBe('req-123')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('uses the generate callback when missing a header', async () => {
|
|
33
|
-
const router = new Router()
|
|
34
|
-
const seenExtra: { prefix: string; hotReloadCounter: number; requestCounter: number }[] = []
|
|
35
|
-
const generated: string[] = []
|
|
36
|
-
const generate = (
|
|
37
|
-
_ctx: unknown,
|
|
38
|
-
extra: { prefix: string; hotReloadCounter: number; requestCounter: number },
|
|
39
|
-
) => {
|
|
40
|
-
seenExtra.push(extra)
|
|
41
|
-
const value = `${extra.prefix}.${extra.hotReloadCounter}.${extra.requestCounter}`
|
|
42
|
-
generated.push(value)
|
|
43
|
-
return value
|
|
44
|
-
}
|
|
45
|
-
router.useRequest(requestIdCtx({ generate, prefix: 'custom' }))
|
|
46
|
-
router.add({
|
|
47
|
-
method: HttpMethod.GET,
|
|
48
|
-
path: '/',
|
|
49
|
-
handler: ({ requestId }) => {
|
|
50
|
-
expect(requestId).toBe(generated[generated.length - 1])
|
|
51
|
-
return new Response(requestId)
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const first = await router.fetch(makeRequest())
|
|
56
|
-
const second = await router.fetch(makeRequest())
|
|
57
|
-
|
|
58
|
-
expect(await first.text()).toMatch(/^custom\.\d+\.\d+$/)
|
|
59
|
-
expect(await second.text()).toMatch(/^custom\.\d+\.\d+$/)
|
|
60
|
-
expect(seenExtra).toHaveLength(2)
|
|
61
|
-
expect(seenExtra[0]?.prefix).toBe('custom')
|
|
62
|
-
expect(seenExtra[1]?.prefix).toBe('custom')
|
|
63
|
-
expect(seenExtra[0]?.hotReloadCounter).toBe(seenExtra[1]?.hotReloadCounter)
|
|
64
|
-
expect(seenExtra[0]?.requestCounter).toBe(1)
|
|
65
|
-
expect(seenExtra[1]?.requestCounter).toBe(2)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('respects header order when multiple names are provided', async () => {
|
|
69
|
-
const router = new Router()
|
|
70
|
-
router.useRequest(requestIdCtx({ readHeaderName: ['x-request-id', 'x-trace-id'] }))
|
|
71
|
-
router.add({
|
|
72
|
-
method: HttpMethod.GET,
|
|
73
|
-
path: '/',
|
|
74
|
-
handler: ({ requestId, request }) => {
|
|
75
|
-
const headerId =
|
|
76
|
-
request.headers.get('x-request-id') ?? request.headers.get('x-trace-id')
|
|
77
|
-
expect(requestId).toBe(headerId)
|
|
78
|
-
return new Response(requestId)
|
|
79
|
-
},
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const fallback = await router.fetch(makeRequest({ 'x-trace-id': 'trace-456' }))
|
|
83
|
-
const preferred = await router.fetch(
|
|
84
|
-
makeRequest({
|
|
85
|
-
'x-request-id': 'primary-789',
|
|
86
|
-
'x-trace-id': 'secondary-000',
|
|
87
|
-
}),
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
expect(await fallback.text()).toBe('trace-456')
|
|
91
|
-
expect(await preferred.text()).toBe('primary-789')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('writes the request id into the response header when configured', async () => {
|
|
95
|
-
const router = new Router()
|
|
96
|
-
router.useRequest(
|
|
97
|
-
requestIdCtx({
|
|
98
|
-
generate: () => 'req-42',
|
|
99
|
-
writeHeaderName: 'x-request-id',
|
|
100
|
-
}),
|
|
101
|
-
)
|
|
102
|
-
router.add({
|
|
103
|
-
method: HttpMethod.GET,
|
|
104
|
-
path: '/',
|
|
105
|
-
handler: ({ requestId }) => {
|
|
106
|
-
expect(requestId).toBe('req-42')
|
|
107
|
-
return 'ok'
|
|
108
|
-
},
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
const response = await router.fetch(makeRequest())
|
|
112
|
-
|
|
113
|
-
expect(response.headers.get('x-request-id')).toBe('req-42')
|
|
114
|
-
expect(await response.text()).toBe('ok')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('writes the request id into logical response headers', async () => {
|
|
118
|
-
const router = new Router()
|
|
119
|
-
router.useRequest(
|
|
120
|
-
requestIdCtx({
|
|
121
|
-
generate: () => 'req-logical',
|
|
122
|
-
writeHeaderName: 'x-request-id',
|
|
123
|
-
}),
|
|
124
|
-
)
|
|
125
|
-
router.add({
|
|
126
|
-
method: HttpMethod.GET,
|
|
127
|
-
path: '/',
|
|
128
|
-
handler: () => ({ ok: true }),
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const response = await router.fetch(makeRequest())
|
|
132
|
-
|
|
133
|
-
expect(response.headers.get('x-request-id')).toBe('req-logical')
|
|
134
|
-
expect(await response.json()).toEqual({ ok: true })
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('uses the default generator format when no header is provided', async () => {
|
|
138
|
-
const router = new Router()
|
|
139
|
-
router.useRequest(requestIdCtx({ prefix: 'req' }))
|
|
140
|
-
router.add({
|
|
141
|
-
method: HttpMethod.GET,
|
|
142
|
-
path: '/',
|
|
143
|
-
handler: ({ requestId }) => {
|
|
144
|
-
return new Response(requestId)
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
const first = await router.fetch(makeRequest())
|
|
149
|
-
const second = await router.fetch(makeRequest())
|
|
150
|
-
|
|
151
|
-
const firstId = await first.text()
|
|
152
|
-
const secondId = await second.text()
|
|
153
|
-
|
|
154
|
-
expect(firstId).toBe('req.1')
|
|
155
|
-
expect(secondId).toBe('req.2')
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('does not declare a terminal response while decorating downstream output', () => {
|
|
159
|
-
const router = new Router().useRequest(requestIdCtx({ writeHeaderName: 'x-request-id' }))
|
|
160
|
-
router.add({ method: HttpMethod.GET, path: '/', handler: () => new Response() })
|
|
161
|
-
|
|
162
|
-
expect(router.getRoutes()[0]?.schema).toBeUndefined()
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('writes correlated ids on generated responses', async () => {
|
|
166
|
-
const router = new Router()
|
|
167
|
-
.useRequest(
|
|
168
|
-
requestIdCtx({
|
|
169
|
-
generate: () => 'req-generated',
|
|
170
|
-
writeHeaderName: 'x-request-id',
|
|
171
|
-
}),
|
|
172
|
-
)
|
|
173
|
-
.get('/exists', () => new Response('ok'))
|
|
174
|
-
|
|
175
|
-
const missing = await router.fetch(new Request('https://example.com/missing'))
|
|
176
|
-
const options = await router.fetch(
|
|
177
|
-
new Request('https://example.com/exists', { method: HttpMethod.OPTIONS }),
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
expect(missing.headers.get('x-request-id')).toBe('req-generated')
|
|
181
|
-
expect(options.headers.get('x-request-id')).toBe('req-generated')
|
|
182
|
-
})
|
|
183
|
-
})
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import type { AnyContext, HandlerContext, OneOrMany, RequestMiddleware } from '../types'
|
|
2
|
-
|
|
3
|
-
declare global {
|
|
4
|
-
var _reloadCounter: number
|
|
5
|
-
var _globalRequestCounter: number
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
globalThis._reloadCounter = globalThis._reloadCounter == null ? 0 : globalThis._reloadCounter + 1
|
|
9
|
-
globalThis._globalRequestCounter ??= 0
|
|
10
|
-
|
|
11
|
-
interface ExtraContext {
|
|
12
|
-
prefix: string
|
|
13
|
-
/**
|
|
14
|
-
* Number of times this server has been hot-reloaded.
|
|
15
|
-
*/
|
|
16
|
-
hotReloadCounter: number
|
|
17
|
-
/**
|
|
18
|
-
* Sequential request number for this middleware instance.
|
|
19
|
-
*/
|
|
20
|
-
requestCounter: number
|
|
21
|
-
/**
|
|
22
|
-
* Sequential request number across request id middleware instances.
|
|
23
|
-
*/
|
|
24
|
-
globalRequestCounter: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Options for [`requestIdCtx`]{@link requestIdCtx}.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* ```ts
|
|
32
|
-
* const options: RequestIdCtxOptions = {
|
|
33
|
-
* writeHeaderName: 'x-request-id',
|
|
34
|
-
* generate: () => crypto.randomUUID(),
|
|
35
|
-
* }
|
|
36
|
-
* ```
|
|
37
|
-
*
|
|
38
|
-
* @typeParam Ctx - Context available before request id generation.
|
|
39
|
-
*/
|
|
40
|
-
export interface RequestIdCtxOptions<Ctx extends object = AnyContext> {
|
|
41
|
-
/**
|
|
42
|
-
* Prefix used by the default generator. Defaults to an empty string.
|
|
43
|
-
*/
|
|
44
|
-
prefix?: string
|
|
45
|
-
/**
|
|
46
|
-
* Header(s) to check for a request id. Defaults to common request/trace headers.
|
|
47
|
-
* Set to `null` or an empty array to disable header reads.
|
|
48
|
-
*/
|
|
49
|
-
readHeaderName?: OneOrMany<string> | null
|
|
50
|
-
/**
|
|
51
|
-
* Response header name to write the request id into.
|
|
52
|
-
*/
|
|
53
|
-
writeHeaderName?: string
|
|
54
|
-
/**
|
|
55
|
-
* Custom request id generator used when no read header is present.
|
|
56
|
-
*
|
|
57
|
-
* @param ctx - Current request context.
|
|
58
|
-
* @param extra - Prefix and counter values maintained by this middleware.
|
|
59
|
-
* @returns Request identifier.
|
|
60
|
-
*/
|
|
61
|
-
generate?: (ctx: HandlerContext<Ctx>, extra: ExtraContext) => string
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const isHMR = import.meta.hot !== undefined || process.execArgv.includes('--hot')
|
|
65
|
-
|
|
66
|
-
function defaultRequestIdGenerator(_ctx: HandlerContext<AnyContext>, extra: ExtraContext): string {
|
|
67
|
-
let requestId = extra.requestCounter.toString(36)
|
|
68
|
-
if (isHMR) {
|
|
69
|
-
requestId = extra.hotReloadCounter.toString(36) + '.' + requestId
|
|
70
|
-
}
|
|
71
|
-
if (extra.prefix) requestId = `${extra.prefix}.${requestId}`
|
|
72
|
-
return requestId
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Attach a correlated request id to every final response and contextual logger record.
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```ts
|
|
80
|
-
* router.useRequest(requestIdCtx({
|
|
81
|
-
* readHeaderName: ['x-request-id', 'x-trace-id'],
|
|
82
|
-
* writeHeaderName: 'x-request-id',
|
|
83
|
-
* generate: () => crypto.randomUUID(),
|
|
84
|
-
* }))
|
|
85
|
-
* ```
|
|
86
|
-
*
|
|
87
|
-
* @param options - Configuration for reading, generating, and writing request ids.
|
|
88
|
-
* @returns Request-boundary middleware that populates `requestId` and logger context.
|
|
89
|
-
* @typeParam Ctx - Context available before request id generation.
|
|
90
|
-
*/
|
|
91
|
-
export function requestIdCtx<Ctx extends object = AnyContext>(
|
|
92
|
-
options: RequestIdCtxOptions<Ctx> = {},
|
|
93
|
-
): RequestMiddleware<{ requestId: string }, Ctx> {
|
|
94
|
-
const prefix = options.prefix ?? ''
|
|
95
|
-
const headers =
|
|
96
|
-
options.readHeaderName === undefined
|
|
97
|
-
? ['x-request-id', 'x-trace-id', 'traceparent']
|
|
98
|
-
: options.readHeaderName == null
|
|
99
|
-
? []
|
|
100
|
-
: Array.isArray(options.readHeaderName)
|
|
101
|
-
? options.readHeaderName
|
|
102
|
-
: [options.readHeaderName]
|
|
103
|
-
const writeHeaderName = options.writeHeaderName
|
|
104
|
-
const hotReloadCounter = globalThis._reloadCounter
|
|
105
|
-
let requestCounter = 0
|
|
106
|
-
|
|
107
|
-
return async (ctx, next) => {
|
|
108
|
-
let headerId: string | null = null
|
|
109
|
-
|
|
110
|
-
for (const name of headers) {
|
|
111
|
-
headerId = ctx.request.headers.get(name)
|
|
112
|
-
if (headerId !== null) break
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const extra: ExtraContext = {
|
|
116
|
-
prefix,
|
|
117
|
-
hotReloadCounter,
|
|
118
|
-
requestCounter: ++requestCounter,
|
|
119
|
-
globalRequestCounter: ++globalThis._globalRequestCounter,
|
|
120
|
-
}
|
|
121
|
-
const requestId =
|
|
122
|
-
headerId ??
|
|
123
|
-
options.generate?.(ctx, extra) ??
|
|
124
|
-
defaultRequestIdGenerator(ctx, extra)
|
|
125
|
-
|
|
126
|
-
ctx.requestId = requestId
|
|
127
|
-
ctx.logger = ctx.logger.withContext({ 'request.id': requestId })
|
|
128
|
-
|
|
129
|
-
const response = await next()
|
|
130
|
-
if (writeHeaderName != null) {
|
|
131
|
-
response.headers.set(writeHeaderName, requestId)
|
|
132
|
-
}
|
|
133
|
-
return response
|
|
134
|
-
}
|
|
135
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
import { formatDurationMilliseconds } from './request-logger-format.ts'
|
|
3
|
-
|
|
4
|
-
describe(formatDurationMilliseconds.name, () => {
|
|
5
|
-
test('formats durations correctly', () => {
|
|
6
|
-
expect(formatDurationMilliseconds(0.005)).toBe('0.01')
|
|
7
|
-
expect(formatDurationMilliseconds(0.05)).toBe('0.05')
|
|
8
|
-
expect(formatDurationMilliseconds(0.5)).toBe('0.50')
|
|
9
|
-
expect(formatDurationMilliseconds(5)).toBe('5.0')
|
|
10
|
-
expect(formatDurationMilliseconds(50)).toBe('50.0')
|
|
11
|
-
expect(formatDurationMilliseconds(500)).toBe('500')
|
|
12
|
-
|
|
13
|
-
expect(formatDurationMilliseconds(0.99)).toBe('0.99')
|
|
14
|
-
expect(formatDurationMilliseconds(0.996)).toBe('1.0')
|
|
15
|
-
})
|
|
16
|
-
})
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import type { LogRecord, LogRecordTransform, TerminalLogRecordFormatter } from '@mpen/logger'
|
|
2
|
-
|
|
3
|
-
export const ROUTEKIT_HTTP_SERVER_REQUEST_EVENT_NAME = 'http.server.request'
|
|
4
|
-
|
|
5
|
-
const ROUTEKIT_REQUEST_CONTEXT_KEYS = ['http.request.method', 'url.path', 'request.id'] as const
|
|
6
|
-
|
|
7
|
-
const REQUEST_ID_COLOR_KEYS = [
|
|
8
|
-
'cyanBright',
|
|
9
|
-
'magentaBright',
|
|
10
|
-
'greenBright',
|
|
11
|
-
'yellowBright',
|
|
12
|
-
'blueBright',
|
|
13
|
-
'redBright',
|
|
14
|
-
'cyan',
|
|
15
|
-
'magenta',
|
|
16
|
-
] as const
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Transform Routekit request activity records into production JSON access logs.
|
|
20
|
-
*
|
|
21
|
-
* Removes the fallback activity message from `http.server.request` records while preserving
|
|
22
|
-
* normal application log messages written through the same contextual logger.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* import { JsonLogger } from '@mpen/logger'
|
|
27
|
-
* import { transformRoutekitJsonLogRecord } from '@mpen/routekit/middleware'
|
|
28
|
-
*
|
|
29
|
-
* const logger = new JsonLogger({
|
|
30
|
-
* transformRecord: transformRoutekitJsonLogRecord,
|
|
31
|
-
* })
|
|
32
|
-
* ```
|
|
33
|
-
*
|
|
34
|
-
* @param record - Record emitted by a logger.
|
|
35
|
-
* @returns The transformed record, or `undefined` to use the original record.
|
|
36
|
-
*/
|
|
37
|
-
export const transformRoutekitJsonLogRecord: LogRecordTransform = (record) => {
|
|
38
|
-
if (!isRoutekitRequestEvent(record)) {
|
|
39
|
-
return undefined
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
...record,
|
|
44
|
-
data: stripActivityLifecycleMessage(record),
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Format Routekit request context for terminal logs.
|
|
50
|
-
*
|
|
51
|
-
* Renders compact request start/completion lines, color-correlated request identifiers, status
|
|
52
|
-
* codes, elapsed time, and response sizes while leaving unrelated records to
|
|
53
|
-
* [`TerminalLogger`]{@link import('@mpen/logger').TerminalLogger}'s default renderer.
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```ts
|
|
57
|
-
* import { TerminalLogger } from '@mpen/logger'
|
|
58
|
-
* import { formatRoutekitTerminalLogRecord } from '@mpen/routekit/middleware'
|
|
59
|
-
*
|
|
60
|
-
* const logger = new TerminalLogger({
|
|
61
|
-
* formatRecord: formatRoutekitTerminalLogRecord,
|
|
62
|
-
* })
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* @param record - Record emitted by a logger.
|
|
66
|
-
* @param terminal - Terminal formatting helpers.
|
|
67
|
-
* @returns Formatted terminal output, or `undefined` to use the default renderer.
|
|
68
|
-
*/
|
|
69
|
-
export const formatRoutekitTerminalLogRecord: TerminalLogRecordFormatter = (record, terminal) => {
|
|
70
|
-
if (!hasRoutekitRequestContext(record)) {
|
|
71
|
-
return undefined
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const colors = terminal.colors
|
|
75
|
-
const context = record.context
|
|
76
|
-
const requestId = getStringContext(record, 'request.id')
|
|
77
|
-
const method = getStringContext(record, 'http.request.method')!
|
|
78
|
-
const path = getStringContext(record, 'url.path')!
|
|
79
|
-
const route = getStringContext(record, 'http.route')
|
|
80
|
-
const direction = getActivityDirection(record)
|
|
81
|
-
const parts = [
|
|
82
|
-
formatServiceName(getStringContext(record, 'service.name'), colors),
|
|
83
|
-
requestId == null ? undefined : formatRequestId(requestId, colors),
|
|
84
|
-
requestId == null ? undefined : formatDirection(direction, colors),
|
|
85
|
-
requestId == null ? formatDirection(direction, colors) : undefined,
|
|
86
|
-
colors.bold(colors.whiteBright(method)),
|
|
87
|
-
colors.white(path),
|
|
88
|
-
route == null || route === path ? undefined : colors.blackBright(`route=${route}`),
|
|
89
|
-
formatStatusCode(getNumberContext(context['http.response.status_code']), colors),
|
|
90
|
-
formatDuration(getNumberContext(context.duration_ms), colors),
|
|
91
|
-
formatBodySize(getNumberContext(context['http.response.body.size']), colors),
|
|
92
|
-
].filter((part): part is string => part != null && part !== '')
|
|
93
|
-
const prefix = `${terminal.icon} ${colors.blackBright(terminal.time)} ${parts.join(' ')}`
|
|
94
|
-
const data = direction == null ? record.data : stripActivityLifecycleMessage(record)
|
|
95
|
-
|
|
96
|
-
if (data.length === 0) {
|
|
97
|
-
return prefix.trimEnd()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const message = terminal.formatData(data)
|
|
101
|
-
|
|
102
|
-
if (message === '') {
|
|
103
|
-
return prefix.trimEnd()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const [firstLine = '', ...remainingLines] = message.split('\n')
|
|
107
|
-
|
|
108
|
-
return [prefix + ' ' + firstLine, ...remainingLines.map((line) => ' ' + line)].join('\n')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function hasRoutekitRequestContext(record: LogRecord): boolean {
|
|
112
|
-
return ROUTEKIT_REQUEST_CONTEXT_KEYS.every((key) => record.context[key] != null)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isRoutekitRequestEvent(record: LogRecord): boolean {
|
|
116
|
-
return record.context['event.name'] === ROUTEKIT_HTTP_SERVER_REQUEST_EVENT_NAME
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function stripActivityLifecycleMessage(record: LogRecord): readonly unknown[] {
|
|
120
|
-
return record.metadata.activity == null ? record.data : record.data.slice(1)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function getActivityDirection(record: LogRecord): '\u2192' | '\u2190' | undefined {
|
|
124
|
-
switch (record.metadata.activity?.phase) {
|
|
125
|
-
case 'start':
|
|
126
|
-
return '\u2192'
|
|
127
|
-
case 'end':
|
|
128
|
-
return '\u2190'
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function getStringContext(record: LogRecord, key: string): string | undefined {
|
|
133
|
-
const value = record.context[key]
|
|
134
|
-
|
|
135
|
-
return value == null ? undefined : String(value)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function getNumberContext(value: unknown): number | undefined {
|
|
139
|
-
if (typeof value !== 'number') {
|
|
140
|
-
return undefined
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return Number.isFinite(value) ? value : undefined
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function formatServiceName(
|
|
147
|
-
value: string | undefined,
|
|
148
|
-
colors: Parameters<TerminalLogRecordFormatter>[1]['colors'],
|
|
149
|
-
): string | undefined {
|
|
150
|
-
if (value == null) {
|
|
151
|
-
return undefined
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const text = `[${value}]`
|
|
155
|
-
|
|
156
|
-
return colors.bold(colors.blueBright(text))
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function formatRequestId(
|
|
160
|
-
value: string,
|
|
161
|
-
colors: Parameters<TerminalLogRecordFormatter>[1]['colors'],
|
|
162
|
-
): string {
|
|
163
|
-
const text = `[${value}]`
|
|
164
|
-
let hash = 0
|
|
165
|
-
|
|
166
|
-
for (const char of value) {
|
|
167
|
-
hash = (hash * 31 + char.codePointAt(0)!) >>> 0
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const color = REQUEST_ID_COLOR_KEYS[hash % REQUEST_ID_COLOR_KEYS.length]!
|
|
171
|
-
|
|
172
|
-
return colors[color](text)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function formatDirection(
|
|
176
|
-
value: '\u2192' | '\u2190' | undefined,
|
|
177
|
-
colors: Parameters<TerminalLogRecordFormatter>[1]['colors'],
|
|
178
|
-
): string | undefined {
|
|
179
|
-
return value == null ? undefined : colors.bold(colors.whiteBright(value))
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function formatStatusCode(
|
|
183
|
-
value: number | undefined,
|
|
184
|
-
colors: Parameters<TerminalLogRecordFormatter>[1]['colors'],
|
|
185
|
-
): string | undefined {
|
|
186
|
-
if (value == null) {
|
|
187
|
-
return undefined
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const text = String(value)
|
|
191
|
-
const formatted =
|
|
192
|
-
value >= 500
|
|
193
|
-
? colors.redBright(text)
|
|
194
|
-
: value >= 400
|
|
195
|
-
? colors.yellowBright(text)
|
|
196
|
-
: value >= 300
|
|
197
|
-
? colors.cyanBright(text)
|
|
198
|
-
: colors.greenBright(text)
|
|
199
|
-
|
|
200
|
-
return colors.bold(formatted)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function formatDuration(
|
|
204
|
-
value: number | undefined,
|
|
205
|
-
colors: Parameters<TerminalLogRecordFormatter>[1]['colors'],
|
|
206
|
-
): string | undefined {
|
|
207
|
-
if (value == null) {
|
|
208
|
-
return undefined
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const text = `${formatDurationMilliseconds(value)}ms`
|
|
212
|
-
|
|
213
|
-
if (value >= 1000) {
|
|
214
|
-
return colors.redBright(text)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return value >= 250 ? colors.yellowBright(text) : colors.greenBright(text)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* @internal
|
|
222
|
-
*/
|
|
223
|
-
export function formatDurationMilliseconds(value: number): string {
|
|
224
|
-
const p2 = value.toFixed(2)
|
|
225
|
-
if (Number(p2) < 1) {
|
|
226
|
-
return p2
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const p1 = value.toFixed(1)
|
|
230
|
-
if (Number(p1) < 100) {
|
|
231
|
-
return p1
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return value.toFixed(0)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function formatBodySize(
|
|
238
|
-
value: number | undefined,
|
|
239
|
-
colors: Parameters<TerminalLogRecordFormatter>[1]['colors'],
|
|
240
|
-
): string | undefined {
|
|
241
|
-
return value == null ? undefined : colors.cyan(formatByteSize(value))
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function formatByteSize(value: number): string {
|
|
245
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
246
|
-
return String(value)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (value < 1024) {
|
|
250
|
-
return `${value} B`
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const units = ['KiB', 'MiB', 'GiB'] as const
|
|
254
|
-
let amount = value
|
|
255
|
-
let unit = 'B'
|
|
256
|
-
|
|
257
|
-
for (const nextUnit of units) {
|
|
258
|
-
amount /= 1024
|
|
259
|
-
unit = nextUnit
|
|
260
|
-
|
|
261
|
-
if (amount < 1024 || nextUnit === units.at(-1)) {
|
|
262
|
-
break
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const precision = amount >= 100 ? 0 : amount >= 10 ? 1 : 2
|
|
267
|
-
|
|
268
|
-
return `${amount.toFixed(precision)} ${unit}`
|
|
269
|
-
}
|