@znight/http 0.0.0-stage → 0.1.0

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2333 -2
  3. package/dist/adapter-CGSceVEN.d.mts +876 -0
  4. package/dist/compiler.d.mts +156 -0
  5. package/dist/compiler.mjs +2180 -0
  6. package/dist/compiler.mjs.map +1 -0
  7. package/dist/express.d.mts +39 -0
  8. package/dist/express.mjs +135 -0
  9. package/dist/express.mjs.map +1 -0
  10. package/dist/fastify.d.mts +33 -0
  11. package/dist/fastify.mjs +191 -0
  12. package/dist/fastify.mjs.map +1 -0
  13. package/dist/hono.d.mts +49 -0
  14. package/dist/hono.mjs +319 -0
  15. package/dist/hono.mjs.map +1 -0
  16. package/dist/index.d.mts +1254 -0
  17. package/dist/index.mjs +5618 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/node-BUnkdSsa.mjs +183 -0
  20. package/dist/node-BUnkdSsa.mjs.map +1 -0
  21. package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
  22. package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
  23. package/dist/node.d.mts +30 -0
  24. package/dist/node.mjs +1 -0
  25. package/dist/path-JI87C4k8.d.mts +109 -0
  26. package/dist/path.d.mts +2 -0
  27. package/dist/path.mjs +245 -0
  28. package/dist/path.mjs.map +1 -0
  29. package/dist/size-D8rsQcV0.mjs +184 -0
  30. package/dist/size-D8rsQcV0.mjs.map +1 -0
  31. package/dist/utils-DCbu3a4w.mjs +27 -0
  32. package/dist/utils-DCbu3a4w.mjs.map +1 -0
  33. package/package.json +142 -4
  34. package/src/adapter.ts +240 -0
  35. package/src/adapters/express.ts +201 -0
  36. package/src/adapters/fastify.ts +280 -0
  37. package/src/adapters/hono.ts +488 -0
  38. package/src/adapters/node-io.ts +368 -0
  39. package/src/adapters/node.ts +135 -0
  40. package/src/adapters/shared.ts +508 -0
  41. package/src/body.ts +135 -0
  42. package/src/coerce.ts +195 -0
  43. package/src/compiler/analyze.ts +1513 -0
  44. package/src/compiler/ast.ts +90 -0
  45. package/src/compiler/coercion.ts +267 -0
  46. package/src/compiler/generate.ts +374 -0
  47. package/src/compiler/index.ts +76 -0
  48. package/src/compiler/model.ts +260 -0
  49. package/src/compiler/route-decorators.ts +405 -0
  50. package/src/compiler/validate.ts +288 -0
  51. package/src/context.ts +533 -0
  52. package/src/cookie-settings.ts +68 -0
  53. package/src/cookie-signer.ts +106 -0
  54. package/src/cookies.ts +726 -0
  55. package/src/cors.ts +349 -0
  56. package/src/decisions.ts +331 -0
  57. package/src/decorators.ts +486 -0
  58. package/src/event-channel.ts +197 -0
  59. package/src/exceptions.ts +257 -0
  60. package/src/files.ts +429 -0
  61. package/src/headers.ts +39 -0
  62. package/src/index.ts +185 -0
  63. package/src/inject-sse.ts +309 -0
  64. package/src/interfaces.ts +101 -0
  65. package/src/metadata.ts +162 -0
  66. package/src/mime.ts +157 -0
  67. package/src/multipart.ts +597 -0
  68. package/src/option-error.ts +31 -0
  69. package/src/path.ts +288 -0
  70. package/src/pipeline.ts +918 -0
  71. package/src/plugin-routes.ts +188 -0
  72. package/src/plugin.ts +1432 -0
  73. package/src/proxy.ts +236 -0
  74. package/src/query.ts +30 -0
  75. package/src/response.ts +240 -0
  76. package/src/router.ts +119 -0
  77. package/src/security-headers.ts +161 -0
  78. package/src/serialize.ts +264 -0
  79. package/src/size.ts +66 -0
  80. package/src/sse-parse.ts +194 -0
  81. package/src/sse.ts +650 -0
  82. package/src/standard-schema.ts +81 -0
  83. package/src/status.ts +78 -0
  84. package/src/stream.ts +200 -0
  85. package/src/tokens.ts +21 -0
  86. package/src/uploads.ts +176 -0
  87. package/src/utils.ts +31 -0
  88. package/src/validate.ts +65 -0
@@ -0,0 +1,374 @@
1
+ /**
2
+ * `generate` of the `@znight/http` compiler extension: the `metadata.extensions.http` section
3
+ * (README, "Compiler contract"). Controllers are sorted by token, routes keep their source order and
4
+ * every route gets an `invoke(c, ctx)` arrow extracting, coercing and validating its arguments.
5
+ */
6
+ import type { ClassRef, GenerateContext, GenInput, GenNode, ValueRef } from '@znight/compiler'
7
+ import type {
8
+ CoercionPlan,
9
+ HttpControllerData,
10
+ HttpParamData,
11
+ HttpRouteData,
12
+ MetaEntry,
13
+ ParamCoercion,
14
+ SseRouteData,
15
+ UploadData,
16
+ } from './model.ts'
17
+
18
+ import { compareStrings } from '../path.ts'
19
+ import { HTTP_PACKAGE } from './model.ts'
20
+
21
+ type Context = GenerateContext<HttpControllerData>
22
+
23
+ /** `uploads` helper by kind and presence (`@UploadedFile` / `@UploadedFiles`, required or not). */
24
+ const UPLOAD_HELPERS = {
25
+ file: { required: 'file', optional: 'optionalFile' },
26
+ files: { required: 'files', optional: 'optionalFiles' },
27
+ } as const
28
+
29
+ /** Imports of `@znight/http` helpers, created on first use (the generated module has no unused import). */
30
+ interface Helpers {
31
+ coerce(): GenNode
32
+ validate(): GenNode
33
+ uploads(): GenNode
34
+ context(): GenNode
35
+ }
36
+
37
+ /** Generates `metadata.extensions.http` (`undefined` when the application has no controller). */
38
+ export function generateHttp(ctx: Context): GenNode | undefined {
39
+ if (ctx.stored.length === 0) {
40
+ return undefined
41
+ }
42
+ const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token))
43
+ const helpers = createHelpers(ctx)
44
+ const entries: [string, GenInput][] = [
45
+ ['controllers', ctx.arr(controllers.map(controller => controllerNode(ctx, helpers, controller)))],
46
+ ]
47
+ const groups = groupsNode(ctx, controllers)
48
+ if (groups !== undefined) {
49
+ entries.push(['groups', groups])
50
+ }
51
+ return ctx.obj(entries)
52
+ }
53
+
54
+ /**
55
+ * `groups`: the route decorator groups the runtime needs (required, holding several keys, or named
56
+ * differently from their key), sorted, with sorted keys; `undefined` when there is none, so that
57
+ * applications without groups keep a byte-identical output.
58
+ */
59
+ function groupsNode(ctx: Context, controllers: readonly HttpControllerData[]): GenNode | undefined {
60
+ const groups = new Map<string, { readonly keys: Set<string>; required: boolean }>()
61
+ const visit = (entries: readonly MetaEntry[]): void => {
62
+ for (const entry of entries) {
63
+ const group = groups.get(entry.group)
64
+ if (group === undefined) {
65
+ groups.set(entry.group, { keys: new Set([entry.key]), required: entry.required })
66
+ } else {
67
+ group.keys.add(entry.key)
68
+ group.required ||= entry.required
69
+ }
70
+ }
71
+ }
72
+ for (const controller of controllers) {
73
+ visit(controller.meta)
74
+ for (const route of controller.routes) {
75
+ visit(route.meta)
76
+ }
77
+ }
78
+ const needed = [...groups]
79
+ .filter(([name, group]) => group.required || group.keys.size > 1 || !group.keys.has(name))
80
+ .sort(([a], [b]) => compareStrings(a, b))
81
+ if (needed.length === 0) {
82
+ return undefined
83
+ }
84
+ return ctx.obj(
85
+ needed.map(
86
+ ([name, group]) =>
87
+ [
88
+ name,
89
+ ctx.obj([
90
+ ['keys', ctx.lit([...group.keys].sort(compareStrings))],
91
+ ['required', group.required],
92
+ ]),
93
+ ] as const,
94
+ ),
95
+ )
96
+ }
97
+
98
+ function createHelpers(ctx: Context): Helpers {
99
+ let coerce: GenNode | undefined
100
+ let validate: GenNode | undefined
101
+ let uploads: GenNode | undefined
102
+ let context: GenNode | undefined
103
+ return {
104
+ coerce: () => {
105
+ coerce ??= ctx.importFrom(HTTP_PACKAGE, 'coerce')
106
+ return coerce
107
+ },
108
+ validate: () => {
109
+ validate ??= ctx.importFrom(HTTP_PACKAGE, 'validate')
110
+ return validate
111
+ },
112
+ uploads: () => {
113
+ uploads ??= ctx.importFrom(HTTP_PACKAGE, 'uploads')
114
+ return uploads
115
+ },
116
+ context: () => {
117
+ context ??= ctx.importFrom(HTTP_PACKAGE, 'HttpContext', { type: true })
118
+ return context
119
+ },
120
+ }
121
+ }
122
+
123
+ function controllerNode(ctx: Context, helpers: Helpers, controller: HttpControllerData): GenNode {
124
+ return ctx.obj([
125
+ ['token', controller.token],
126
+ ['basePath', controller.basePath],
127
+ ['guards', tokens(ctx, controller.guards)],
128
+ ['interceptors', tokens(ctx, controller.interceptors)],
129
+ ['meta', metaNode(ctx, controller.meta)],
130
+ ['routes', ctx.arr(controller.routes.map(route => routeNode(ctx, helpers, controller, route)))],
131
+ ])
132
+ }
133
+
134
+ function routeNode(ctx: Context, helpers: Helpers, controller: HttpControllerData, route: HttpRouteData): GenNode {
135
+ const entries: [string, GenInput][] = [
136
+ ['method', route.method],
137
+ ['path', route.path],
138
+ ['fullPath', route.fullPath],
139
+ ['handler', route.handler],
140
+ ]
141
+ if (route.status !== undefined) {
142
+ entries.push(['status', route.status])
143
+ }
144
+ if (route.headers.length > 0) {
145
+ entries.push(['headers', ctx.obj(route.headers.map(([name, value]) => [name, value] as const))])
146
+ }
147
+ entries.push(
148
+ ['meta', metaNode(ctx, route.meta)],
149
+ ['guards', tokens(ctx, route.guards)],
150
+ ['interceptors', tokens(ctx, route.interceptors)],
151
+ ['params', ctx.arr(route.params.map(param => paramNode(ctx, param)))],
152
+ )
153
+ if (route.bodyLimit !== undefined) {
154
+ entries.push(['bodyLimit', route.bodyLimit])
155
+ }
156
+ const uploads = route.params.flatMap(param => (param.upload === undefined ? [] : [param.upload]))
157
+ if (uploads.length > 0) {
158
+ entries.push(['uploads', ctx.arr(uploads.map(upload => uploadNode(ctx, upload)))])
159
+ }
160
+ if (route.rawResponse) {
161
+ entries.push(['rawResponse', true])
162
+ }
163
+ if (route.sse !== undefined) {
164
+ entries.push(['sse', sseNode(ctx, route.sse)])
165
+ }
166
+ entries.push(['invoke', invokeNode(ctx, helpers, controller, route)])
167
+ return ctx.obj(entries)
168
+ }
169
+
170
+ /** `{ heartbeat?, retry?, maxDuration? }` (`{}` without options). */
171
+ function sseNode(ctx: Context, sse: SseRouteData): GenNode {
172
+ const entries: [string, GenInput][] = []
173
+ if (sse.heartbeat !== undefined) {
174
+ entries.push(['heartbeat', sse.heartbeat])
175
+ }
176
+ if (sse.retry !== undefined) {
177
+ entries.push(['retry', sse.retry])
178
+ }
179
+ if (sse.maxDuration !== undefined) {
180
+ entries.push(['maxDuration', sse.maxDuration])
181
+ }
182
+ return ctx.obj(entries)
183
+ }
184
+
185
+ function paramNode(ctx: Context, param: HttpParamData): GenNode {
186
+ const entries: [string, GenInput][] = [
187
+ ['index', param.index],
188
+ ['source', param.source],
189
+ ]
190
+ if (param.key !== undefined) {
191
+ entries.push(['name', param.key])
192
+ }
193
+ if (param.type !== undefined) {
194
+ entries.push(['type', param.type])
195
+ }
196
+ if (param.optional) {
197
+ entries.push(['optional', true])
198
+ }
199
+ if (param.schema !== undefined) {
200
+ entries.push(['schema', true])
201
+ }
202
+ return ctx.obj(entries)
203
+ }
204
+
205
+ /** `{ field?, multiple?, required?, maxSize?, maxFiles?, mimeTypes? }` (absent values omitted). */
206
+ function uploadNode(ctx: Context, upload: UploadData): GenNode {
207
+ const entries: [string, GenInput][] = []
208
+ if (upload.field !== undefined) {
209
+ entries.push(['field', upload.field])
210
+ }
211
+ if (upload.multiple) {
212
+ entries.push(['multiple', true])
213
+ }
214
+ if (upload.required) {
215
+ entries.push(['required', true])
216
+ }
217
+ if (upload.maxSize !== undefined) {
218
+ entries.push(['maxSize', upload.maxSize])
219
+ }
220
+ if (upload.maxFiles !== undefined) {
221
+ entries.push(['maxFiles', upload.maxFiles])
222
+ }
223
+ if (upload.mimeTypes !== undefined) {
224
+ entries.push(['mimeTypes', ctx.lit(upload.mimeTypes)])
225
+ }
226
+ return ctx.obj(entries)
227
+ }
228
+
229
+ function metaNode(ctx: Context, meta: readonly MetaEntry[]): GenNode {
230
+ return ctx.obj(meta.map(entry => [entry.key, ctx.lit(entry.value)] as const))
231
+ }
232
+
233
+ function tokens(ctx: Context, refs: readonly ClassRef[]): GenNode {
234
+ return ctx.arr(refs.map(ref => ctx.tokenOf(ref) ?? ref.token))
235
+ }
236
+
237
+ // -------------------------------------------------------------------------------------------------
238
+ // invoke
239
+ // -------------------------------------------------------------------------------------------------
240
+
241
+ /**
242
+ * `(c: Controller, ctx: HttpContext) => c.handler(...args)`: `async` (awaiting every `validate()`)
243
+ * when the route validates something with a schema, synchronous otherwise.
244
+ */
245
+ function invokeNode(ctx: Context, helpers: Helpers, controller: HttpControllerData, route: HttpRouteData): GenNode {
246
+ const async = route.bodySchema !== undefined || route.params.some(param => param.schema !== undefined)
247
+ const params: [string, GenNode][] = [['c', ctx.importType(controller.identity)]]
248
+ // `ctx` is declared only when used (generated code type-checks with `noUnusedParameters`)
249
+ if (route.params.length > 0 || route.bodySchema !== undefined) {
250
+ params.push(['ctx', helpers.context()])
251
+ }
252
+ return ctx.arrow(
253
+ params,
254
+ (c, http) => {
255
+ const args: GenNode[] = []
256
+ for (const param of route.params) {
257
+ args[param.index] = argumentNode(ctx, helpers, http as GenNode, param)
258
+ }
259
+ const call = ctx.call(
260
+ ctx.member(c, route.handler),
261
+ Array.from(args, arg => arg ?? ctx.lit(undefined)),
262
+ )
263
+ if (route.bodySchema === undefined) {
264
+ return call
265
+ }
266
+ // @Validate(S) without @Body() parameter: the body is validated (and discarded)
267
+ const body = ctx.code`await ${ctx.call(ctx.member(http as GenNode, 'parseBody'))}`
268
+ const check = validation(ctx, helpers, route.bodySchema, body, 'body')
269
+ return ctx.code`{ ${check}; return ${call} }`
270
+ },
271
+ { async },
272
+ )
273
+ }
274
+
275
+ /** The expression passed to the handler for one parameter. */
276
+ function argumentNode(ctx: Context, helpers: Helpers, http: GenNode, param: HttpParamData): GenNode {
277
+ switch (param.source) {
278
+ case 'context':
279
+ return http
280
+ case 'request':
281
+ return ctx.code`${ctx.member(http, 'request')} as never`
282
+ case 'response':
283
+ return ctx.code`${ctx.member(http, 'response')} as never`
284
+ case 'body': {
285
+ const body = ctx.member(http, 'body')
286
+ return param.schema === undefined
287
+ ? ctx.code`${body} as never`
288
+ : validation(ctx, helpers, param.schema, body, 'body')
289
+ }
290
+ case 'file':
291
+ case 'files': {
292
+ const upload = param.upload as UploadData
293
+ const helper = UPLOAD_HELPERS[upload.multiple ? 'files' : 'file'][upload.required ? 'required' : 'optional']
294
+ const call = ctx.call(ctx.member(helpers.uploads(), helper), [http, ctx.lit(upload.field)])
295
+ return upload.cast ? ctx.code`${call} as never` : call
296
+ }
297
+ case 'headers':
298
+ case 'param':
299
+ case 'query': {
300
+ const record = ctx.member(http, param.source === 'param' ? 'params' : param.source)
301
+ if (param.key === undefined) {
302
+ const location = param.source === 'param' ? 'params' : param.source
303
+ return param.schema === undefined
304
+ ? ctx.code`${record} as never`
305
+ : validation(ctx, helpers, param.schema, record, location)
306
+ }
307
+ const value = ctx.code`${record}[${param.key}]`
308
+ const label = `${param.source === 'headers' ? 'header' : param.source} "${param.key}"`
309
+ if (param.schema !== undefined) {
310
+ return validation(ctx, helpers, param.schema, value, label)
311
+ }
312
+ return coercion(ctx, helpers, param.coercion ?? { plan: { kind: 'raw' }, cast: false }, value, label)
313
+ }
314
+ case 'cookie':
315
+ case 'signedCookie': {
316
+ if (param.key === undefined) {
317
+ // @Cookie() / @Cookie(S): every cookie
318
+ const cookies = ctx.member(http, 'cookies')
319
+ return param.schema === undefined
320
+ ? ctx.code`${cookies} as never`
321
+ : validation(ctx, helpers, param.schema, cookies, 'cookies')
322
+ }
323
+ const value =
324
+ param.source === 'cookie'
325
+ ? ctx.code`${ctx.member(http, 'cookies')}[${param.key}]`
326
+ : ctx.call(ctx.member(http, 'signedCookie'), [param.key])
327
+ const label = `cookie "${param.key}"`
328
+ if (param.schema !== undefined) {
329
+ return validation(ctx, helpers, param.schema, value, label)
330
+ }
331
+ return coercion(ctx, helpers, param.coercion ?? { plan: { kind: 'raw' }, cast: false }, value, label)
332
+ }
333
+ }
334
+ }
335
+
336
+ /** `await validate(schema, value, location)`. */
337
+ function validation(ctx: Context, helpers: Helpers, schema: ValueRef, value: GenNode, location: string): GenNode {
338
+ return ctx.code`await ${ctx.call(helpers.validate(), [ctx.ref(schema), value, location])}`
339
+ }
340
+
341
+ /** `coerce.X(value, label, ...)`, cast with `as never` when the result does not have the declared type. */
342
+ function coercion(ctx: Context, helpers: Helpers, coercion: ParamCoercion, value: GenNode, label: string): GenNode {
343
+ const node = coerceCall(ctx, helpers, coercion.plan, value, label)
344
+ return coercion.cast ? ctx.code`${node} as never` : node
345
+ }
346
+
347
+ function coerceCall(ctx: Context, helpers: Helpers, plan: CoercionPlan, value: GenNode, label: GenInput): GenNode {
348
+ const helper = (name: string): GenNode => ctx.member(helpers.coerce(), name)
349
+ switch (plan.kind) {
350
+ case 'raw':
351
+ return ctx.code`${value} as never`
352
+ case 'enum':
353
+ return ctx.call(helper('enum'), [value, label, ctx.lit(plan.values)])
354
+ case 'array':
355
+ case 'optional':
356
+ return ctx.call(helper(plan.kind), [value, label, itemCoercer(ctx, helpers, plan.item)])
357
+ default:
358
+ return ctx.call(helper(plan.kind), [value, label])
359
+ }
360
+ }
361
+
362
+ /** A `Coercer` value: `coerce.number`, or `(v, l) => coerce.enum(v, l, [...])`. */
363
+ function itemCoercer(ctx: Context, helpers: Helpers, plan: CoercionPlan): GenNode {
364
+ switch (plan.kind) {
365
+ case 'enum':
366
+ case 'array':
367
+ case 'optional':
368
+ return ctx.arrow(['v', 'l'], (v, l) => coerceCall(ctx, helpers, plan, v, l))
369
+ case 'raw':
370
+ return ctx.arrow(['v'], v => v)
371
+ default:
372
+ return ctx.member(helpers.coerce(), plan.kind)
373
+ }
374
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `@znight/http/compiler`: the compiler extension of `@znight/http` (architecture §6.5), loaded by
3
+ * `@znight/compiler` through `package.json#znight.compiler`.
4
+ *
5
+ * - `analyzeClass`: `@Controller` classes become providers; their decorated methods become routes
6
+ * (paths, parameters with type-based coercion or Standard Schema validation, cookies, uploaded
7
+ * files, guards, interceptors, status, headers, body limits, `@Sse` event streams,
8
+ * `createRouteDecorator` metadata);
9
+ * - `validate`: route paths, `:param` ↔ `@Param`, duplicate routes (ZNIGHT007);
10
+ * - `generate`: `metadata.extensions.http` with one `invoke` function per route (README,
11
+ * "Compiler contract").
12
+ *
13
+ * This entry only `import type`s from `@znight/compiler` and never loads the HTTP runtime.
14
+ */
15
+ import type { CompilerExtension, DecoratorSpec } from '@znight/compiler'
16
+ import type { HttpControllerData } from './model.ts'
17
+
18
+ import { analyzeHttpClass, EXTENSION_NAME } from './analyze.ts'
19
+ import { generateHttp } from './generate.ts'
20
+ import { Ids, ROUTE_METHODS } from './model.ts'
21
+ import { validateHttp } from './validate.ts'
22
+
23
+ export type {
24
+ CoercionPlan,
25
+ HttpControllerData,
26
+ HttpParamData,
27
+ HttpRouteData,
28
+ MetaEntry,
29
+ ParamCoercion,
30
+ ScalarCoercion,
31
+ SseRouteData,
32
+ UploadData,
33
+ } from './model.ts'
34
+
35
+ const ROUTE: DecoratorSpec = { targets: ['method'], params: ['static'] }
36
+ const NAME_OR_SCHEMA: DecoratorSpec = { targets: ['parameter'], params: ['value', 'value'] }
37
+ const NO_ARGUMENT: DecoratorSpec = { targets: ['parameter'], params: [] }
38
+ const CLASS_REFS: DecoratorSpec = { targets: ['class', 'method'], params: [], rest: 'value' }
39
+ const UPLOAD: DecoratorSpec = { targets: ['parameter'], params: ['static', 'static'] }
40
+
41
+ /** Decorators owned by the extension, by canonical identity (static / value arguments, §4.6). */
42
+ const decorators: Readonly<Record<string, DecoratorSpec>> = Object.freeze({
43
+ [Ids.Controller]: { targets: ['class'], params: ['static'] },
44
+ ...Object.fromEntries([...ROUTE_METHODS.keys()].map(identity => [identity, ROUTE])),
45
+ [Ids.Param]: NAME_OR_SCHEMA,
46
+ [Ids.Query]: NAME_OR_SCHEMA,
47
+ [Ids.Body]: { targets: ['parameter'], params: ['value'] },
48
+ [Ids.Headers]: { targets: ['parameter'], params: ['static'] },
49
+ [Ids.Req]: NO_ARGUMENT,
50
+ [Ids.Res]: NO_ARGUMENT,
51
+ [Ids.Ctx]: NO_ARGUMENT,
52
+ [Ids.HttpCode]: { targets: ['method'], params: ['static'] },
53
+ [Ids.Header]: { targets: ['method'], params: ['static', 'static'] },
54
+ [Ids.UseGuard]: CLASS_REFS,
55
+ [Ids.UseInterceptor]: CLASS_REFS,
56
+ [Ids.Validate]: { targets: ['method'], params: ['value'] },
57
+ [Ids.UploadedFile]: UPLOAD,
58
+ [Ids.UploadedFiles]: UPLOAD,
59
+ [Ids.BodyLimit]: { targets: ['class', 'method'], params: ['static'] },
60
+ [Ids.Cookie]: NAME_OR_SCHEMA,
61
+ [Ids.SignedCookie]: NAME_OR_SCHEMA,
62
+ // after the ROUTE spread: `@Sse(path?, options?)` takes two static arguments
63
+ [Ids.Sse]: { targets: ['method'], params: ['static', 'static'] },
64
+ })
65
+
66
+ /** The `@znight/http` compiler extension. */
67
+ const http: CompilerExtension<HttpControllerData> = Object.freeze({
68
+ apiVersion: 1,
69
+ name: EXTENSION_NAME,
70
+ decorators,
71
+ analyzeClass: analyzeHttpClass,
72
+ validate: validateHttp,
73
+ generate: generateHttp,
74
+ })
75
+
76
+ export default http