@znight/http 0.0.0-stage → 0.1.1

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,2180 @@
1
+ import { a as isMediaTypePattern, n as parseByteSize } from "./size-D8rsQcV0.mjs";
2
+ import { checkRoutePath, compareStrings, joinPaths, normalizePath, parseRoutePath } from "./path.mjs";
3
+ // src/compiler/ast.ts
4
+ /** `true` for an object shaped like an AST node. */
5
+ function isNode(value) {
6
+ return typeof value === "object" && value !== null && typeof value.type === "string" && typeof value.start === "number";
7
+ }
8
+ /** The child node stored under `key`, when it is a node. */
9
+ function child(node, key) {
10
+ const value = node[key];
11
+ return isNode(value) ? value : void 0;
12
+ }
13
+ /** The child nodes stored under `key`, when it is an array. */
14
+ function children(node, key) {
15
+ const value = node[key];
16
+ return Array.isArray(value) ? value.filter(isNode) : [];
17
+ }
18
+ const WRAPPERS = /* @__PURE__ */ new Set([
19
+ "ParenthesizedExpression",
20
+ "TSAsExpression",
21
+ "TSSatisfiesExpression",
22
+ "TSNonNullExpression",
23
+ "TSTypeAssertion",
24
+ "TSInstantiationExpression"
25
+ ]);
26
+ /** Removes parentheses and type-only wrappers (`x as T`, `x satisfies T`, `x!`, `<T>x`). */
27
+ function unwrap(node) {
28
+ let current = node;
29
+ while (WRAPPERS.has(current.type)) {
30
+ const inner = child(current, "expression");
31
+ if (!inner) return current;
32
+ current = inner;
33
+ }
34
+ return current;
35
+ }
36
+ /**
37
+ * The default value expression of a method parameter (`page = 1` → `1`), found in the class
38
+ * declaration by the location of the method key.
39
+ */
40
+ function defaultValueOf(cls, method, param) {
41
+ const classNode = cls.declaration.node;
42
+ if (!isNode(classNode)) return;
43
+ const body = child(classNode, "body");
44
+ if (!body) return;
45
+ for (const member of children(body, "body")) {
46
+ const key = child(member, "key");
47
+ if (!key || key.start !== method.location.start) continue;
48
+ const fn = child(member, "value");
49
+ if (!fn) return;
50
+ let pattern = children(fn, "params").filter((node) => !(node.type === "Identifier" && node["name"] === "this"))[param.index];
51
+ if (pattern?.type === "TSParameterProperty") pattern = child(pattern, "parameter");
52
+ return pattern?.type === "AssignmentPattern" ? child(pattern, "right") : void 0;
53
+ }
54
+ }
55
+ // src/compiler/coercion.ts
56
+ const DATE_IDENTITY = "globalThis#Date";
57
+ const STRING = {
58
+ kind: "primitive",
59
+ name: "string"
60
+ };
61
+ const NUMBER = {
62
+ kind: "primitive",
63
+ name: "number"
64
+ };
65
+ const BOOLEAN = {
66
+ kind: "primitive",
67
+ name: "boolean"
68
+ };
69
+ const BIGINT = {
70
+ kind: "primitive",
71
+ name: "bigint"
72
+ };
73
+ const DATE = {
74
+ kind: "reference",
75
+ identity: DATE_IDENTITY,
76
+ name: "Date",
77
+ typeArgs: []
78
+ };
79
+ /** Annotated or inferred type of a parameter. */
80
+ function parameterType(ctx, cls, method, param) {
81
+ let type = param.type;
82
+ if (param.type.kind === "opaque" && param.type.reason === "missing-annotation") type = param.hasDefault ? inferFromDefault(ctx, cls, method, param) : void 0;
83
+ const nullish = type !== void 0 && hasNullish(type);
84
+ return {
85
+ type,
86
+ optional: param.optional || param.hasDefault || nullish
87
+ };
88
+ }
89
+ /**
90
+ * Plans the coercion of a raw value to `type`. `required` is set for route parameters, which are
91
+ * always present (no `coerce.optional`).
92
+ */
93
+ function planCoercion(ctx, declared, param, required) {
94
+ const type = declared.type;
95
+ if (type === void 0) return {
96
+ ok: false,
97
+ reason: `the type of parameter '${param.name}' is unknown`,
98
+ hints: [`Annotate the parameter: ${param.name}: string`]
99
+ };
100
+ const { type: base } = ctx.types.stripNullish(type);
101
+ if (isRaw(base)) return {
102
+ ok: true,
103
+ coercion: {
104
+ plan: { kind: "raw" },
105
+ cast: false
106
+ }
107
+ };
108
+ const scalar = scalarPlan(ctx, base);
109
+ let plan;
110
+ let cast;
111
+ if ("reason" in scalar) {
112
+ if (base.kind !== "array") return failure(ctx, type, scalar.reason);
113
+ const item = scalarPlan(ctx, base.element);
114
+ if ("reason" in item) return failure(ctx, type, item.reason);
115
+ plan = {
116
+ kind: "array",
117
+ item: item.plan
118
+ };
119
+ cast = item.cast;
120
+ } else {
121
+ plan = scalar.plan;
122
+ cast = scalar.cast;
123
+ }
124
+ if (!required && declared.optional) {
125
+ plan = {
126
+ kind: "optional",
127
+ item: plan
128
+ };
129
+ cast ||= !(param.optional || param.hasDefault || acceptsUndefined(type));
130
+ }
131
+ return {
132
+ ok: true,
133
+ coercion: {
134
+ plan,
135
+ cast
136
+ }
137
+ };
138
+ }
139
+ /** `unknown` and `any`: the raw value is passed as is. */
140
+ function isRaw(type) {
141
+ return type.kind === "primitive" && (type.name === "unknown" || type.name === "any");
142
+ }
143
+ function scalarPlan(ctx, type) {
144
+ switch (type.kind) {
145
+ case "primitive": {
146
+ const kind = primitiveCoercion(type.name);
147
+ return kind ? {
148
+ plan: { kind },
149
+ cast: false
150
+ } : { reason: `'${type.name}' values cannot be read from a request` };
151
+ }
152
+ case "literal": return typeof type.value === "bigint" ? { reason: "bigint literal types are not supported" } : {
153
+ plan: {
154
+ kind: "enum",
155
+ values: [type.value]
156
+ },
157
+ cast: false
158
+ };
159
+ case "union": {
160
+ const values = [];
161
+ for (const member of type.types) if (member.kind === "literal" && typeof member.value !== "bigint") values.push(member.value);
162
+ else if (member.kind === "primitive" && member.name === "boolean") values.push(false, true);
163
+ else return { reason: "only unions of literal types can be coerced" };
164
+ return {
165
+ plan: {
166
+ kind: "enum",
167
+ values
168
+ },
169
+ cast: false
170
+ };
171
+ }
172
+ case "reference": {
173
+ if (type.identity === DATE_IDENTITY) return {
174
+ plan: { kind: "date" },
175
+ cast: false
176
+ };
177
+ const values = enumValues(ctx, type.identity);
178
+ if (values) return values.length === 0 ? { reason: `the enum ${type.name} has no member` } : {
179
+ plan: {
180
+ kind: "enum",
181
+ values
182
+ },
183
+ cast: true
184
+ };
185
+ return { reason: "objects cannot be read from a single request value" };
186
+ }
187
+ case "intersection": {
188
+ const primitives = type.types.filter((member) => member.kind === "primitive" || member.kind === "literal");
189
+ const [primitive] = primitives;
190
+ if (primitives.length === 1 && primitive !== void 0) {
191
+ const inner = scalarPlan(ctx, primitive);
192
+ return "reason" in inner ? inner : {
193
+ plan: inner.plan,
194
+ cast: true
195
+ };
196
+ }
197
+ return { reason: "only branded primitives (`string & { ... }`) can be coerced" };
198
+ }
199
+ case "array": return { reason: "nested arrays cannot be coerced" };
200
+ default: return { reason: "this type cannot be read from a request value" };
201
+ }
202
+ }
203
+ function primitiveCoercion(name) {
204
+ switch (name) {
205
+ case "string":
206
+ case "number":
207
+ case "boolean":
208
+ case "bigint": return name;
209
+ default: return;
210
+ }
211
+ }
212
+ function failure(ctx, type, reason) {
213
+ return {
214
+ ok: false,
215
+ reason: `type '${ctx.types.format(type)}' cannot be coerced (${reason})`,
216
+ hints: ["Supported types: string, number, boolean, bigint, Date, literal unions, enums, branded primitives and arrays of them", "Or validate the value with a Standard Schema"]
217
+ };
218
+ }
219
+ /** Member values of a TypeScript enum, `undefined` when `identity` is not an evaluable enum. */
220
+ function enumValues(ctx, identity) {
221
+ const symbol = ctx.project.lookupIdentity(identity);
222
+ if (symbol.kind !== "declaration" || symbol.declaration.kind !== "enum") return;
223
+ const declaration = symbol.declaration;
224
+ const node = declaration.node;
225
+ const id = isNode(node) ? child(node, "id") : void 0;
226
+ const file = ctx.project.getFile(declaration.file);
227
+ if (!id || !file) return;
228
+ const scope = {
229
+ file,
230
+ namespace: declaration.namespace
231
+ };
232
+ const result = ctx.project.evaluate(scope, id, { refs: "none" });
233
+ if (result.kind !== "value" || typeof result.value !== "object" || result.value === null) return;
234
+ return Object.values(result.value).filter((value) => typeof value === "string" || typeof value === "number");
235
+ }
236
+ function inferFromDefault(ctx, cls, method, param) {
237
+ const node = defaultValueOf(cls, method, param);
238
+ if (!node) return;
239
+ const inner = unwrap(node);
240
+ if (inner.type === "NewExpression") {
241
+ const callee = child(inner, "callee");
242
+ if (callee?.type === "Identifier") return ctx.project.resolveIdentifier(cls.file, callee, "value").identity?.identity === DATE_IDENTITY ? DATE : void 0;
243
+ return;
244
+ }
245
+ const result = ctx.project.evaluate(cls.file, node, { refs: "none" });
246
+ return result.kind === "value" ? typeOfValue(result.value) : void 0;
247
+ }
248
+ function typeOfValue(value) {
249
+ switch (typeof value) {
250
+ case "string": return STRING;
251
+ case "number": return NUMBER;
252
+ case "boolean": return BOOLEAN;
253
+ case "bigint": return BIGINT;
254
+ default: {
255
+ if (!Array.isArray(value) || value.length === 0) return;
256
+ const items = value.map(typeOfValue);
257
+ const [first] = items;
258
+ if (first === void 0 || first.kind !== "primitive" || items.some((item) => item !== first)) return;
259
+ return {
260
+ kind: "array",
261
+ element: first
262
+ };
263
+ }
264
+ }
265
+ }
266
+ function hasNullish(type) {
267
+ return type.kind === "union" && type.types.some(isNullish);
268
+ }
269
+ /** Whether a union accepts `undefined` (`T | undefined`, `T | void`). */
270
+ function acceptsUndefined(type) {
271
+ return type.kind === "union" && type.types.some((member) => member.kind === "primitive" && (member.name === "undefined" || member.name === "void"));
272
+ }
273
+ function isNullish(type) {
274
+ return type.kind === "primitive" && (type.name === "undefined" || type.name === "null" || type.name === "void");
275
+ }
276
+ // src/compiler/model.ts
277
+ /** Package name (prefix of every decorator identity). */
278
+ const HTTP_PACKAGE = "@znight/http";
279
+ /** Canonical identities of the decorators of `@znight/http`. */
280
+ const Ids = Object.freeze({
281
+ Controller: "@znight/http#Controller",
282
+ Param: "@znight/http#Param",
283
+ Query: "@znight/http#Query",
284
+ Body: "@znight/http#Body",
285
+ Headers: "@znight/http#Headers",
286
+ Req: "@znight/http#Req",
287
+ Res: "@znight/http#Res",
288
+ Ctx: "@znight/http#Ctx",
289
+ HttpCode: "@znight/http#HttpCode",
290
+ Header: "@znight/http#Header",
291
+ UseGuard: "@znight/http#UseGuard",
292
+ UseInterceptor: "@znight/http#UseInterceptor",
293
+ Validate: "@znight/http#Validate",
294
+ UploadedFile: "@znight/http#UploadedFile",
295
+ UploadedFiles: "@znight/http#UploadedFiles",
296
+ BodyLimit: "@znight/http#BodyLimit",
297
+ Cookie: "@znight/http#Cookie",
298
+ SignedCookie: "@znight/http#SignedCookie",
299
+ Sse: "@znight/http#Sse",
300
+ createRouteDecorator: "@znight/http#createRouteDecorator",
301
+ RouteDecorator: "@znight/http#RouteDecorator"
302
+ });
303
+ /** Route method decorators → HTTP method (`@Sse` declares a `GET` route). */
304
+ const ROUTE_METHODS = /* @__PURE__ */ new Map([
305
+ ["@znight/http#Get", "GET"],
306
+ ["@znight/http#Post", "POST"],
307
+ ["@znight/http#Put", "PUT"],
308
+ ["@znight/http#Patch", "PATCH"],
309
+ ["@znight/http#Delete", "DELETE"],
310
+ ["@znight/http#Head", "HEAD"],
311
+ ["@znight/http#Options", "OPTIONS"],
312
+ ["@znight/http#All", "ALL"],
313
+ ["@znight/http#Sse", "GET"]
314
+ ]);
315
+ /** Parameter decorators → parameter source. */
316
+ const PARAM_SOURCES = /* @__PURE__ */ new Map([
317
+ [Ids.Param, "param"],
318
+ [Ids.Query, "query"],
319
+ [Ids.Body, "body"],
320
+ [Ids.Headers, "headers"],
321
+ [Ids.Req, "request"],
322
+ [Ids.Res, "response"],
323
+ [Ids.Ctx, "context"],
324
+ [Ids.UploadedFile, "file"],
325
+ [Ids.UploadedFiles, "files"],
326
+ [Ids.Cookie, "cookie"],
327
+ [Ids.SignedCookie, "signedCookie"]
328
+ ]);
329
+ /** Error codes used by the extension (see `ErrorCode` in `@znight/core`). */
330
+ const Codes = Object.freeze({
331
+ MissingProvider: "ZNIGHT001",
332
+ InvalidDecorator: "ZNIGHT005",
333
+ InvalidRoute: "ZNIGHT007",
334
+ UnvalidatedInput: "ZNIGHT017",
335
+ MissingRouteDecision: "ZNIGHT023"
336
+ });
337
+ /**
338
+ * Grammar of an explicit route decorator group name (same as the runtime `createRouteDecorator`).
339
+ * A decorator without `group` is its own group, named after its key (any non-empty key).
340
+ */
341
+ const GROUP_NAME = /^[A-Za-z][\w.-]*$/;
342
+ // src/compiler/route-decorators.ts
343
+ const OTHER = Object.freeze({ kind: "other" });
344
+ /** Resolutions per analysis context (one context per compile). */
345
+ const caches = /* @__PURE__ */ new WeakMap();
346
+ /** Resolves a decorator identity to a route decorator definition (memoized per compile). */
347
+ function resolveRouteDecorator(ctx, identity) {
348
+ let cache = caches.get(ctx);
349
+ if (!cache) {
350
+ cache = /* @__PURE__ */ new Map();
351
+ caches.set(ctx, cache);
352
+ }
353
+ let resolution = cache.get(identity);
354
+ if (!resolution) {
355
+ resolution = resolve(ctx, identity);
356
+ cache.set(identity, resolution);
357
+ }
358
+ return resolution;
359
+ }
360
+ function resolve(ctx, identity) {
361
+ if (identity.startsWith("globalThis#")) return OTHER;
362
+ const symbol = ctx.project.lookupIdentity(identity);
363
+ if (symbol.kind !== "declaration") return OTHER;
364
+ const declaration = symbol.declarations.find((candidate) => candidate.kind === "const");
365
+ const node = declaration?.node;
366
+ const file = declaration ? ctx.project.getFile(declaration.file) : void 0;
367
+ if (!declaration || !isNode(node) || !file) return OTHER;
368
+ const scope = {
369
+ file,
370
+ namespace: declaration.namespace
371
+ };
372
+ const init = child(node, "init");
373
+ if (!init) {
374
+ const annotation = child(child(node, "id") ?? node, "typeAnnotation");
375
+ const type = annotation ? ctx.project.resolveType(scope, annotation) : void 0;
376
+ if (type?.kind === "reference" && type.identity === Ids.RouteDecorator) return {
377
+ kind: "invalid",
378
+ message: `the key of the route decorator ${declaration.name} cannot be read: it is declared without initializer`,
379
+ hints: ["Route decorators must be declared in analysed sources: `export const X = createRouteDecorator(key)`", "For a provider library, publish its sources (\"znight\": { \"library\": \"source\" })"]
380
+ };
381
+ return OTHER;
382
+ }
383
+ const call = unwrap(init);
384
+ const callee = call.type === "CallExpression" ? child(call, "callee") : void 0;
385
+ if (!callee || !isEntityName(unwrap(callee))) return OTHER;
386
+ if (ctx.project.resolveIdentifier(scope, unwrap(callee), "value").identity?.identity !== Ids.createRouteDecorator) return OTHER;
387
+ const definition = file.location(declaration.span);
388
+ const [keyNode, optionsNode, ...extra] = children(call, "arguments");
389
+ if (!keyNode || keyNode.type === "SpreadElement") return {
390
+ kind: "invalid",
391
+ message: `createRouteDecorator() requires a key (${declaration.name})`,
392
+ hints: [`export const ${declaration.name} = createRouteDecorator('${declaration.name.toLowerCase()}')`],
393
+ definition
394
+ };
395
+ const key = ctx.project.evaluate(scope, keyNode, { refs: "none" });
396
+ if (key.kind !== "value" || typeof key.value !== "string" || key.value === "") return {
397
+ kind: "invalid",
398
+ message: `the key of the route decorator ${declaration.name} must be a statically evaluable non-empty string`,
399
+ hints: ["Use a string literal or a `const` string: createRouteDecorator('roles')"],
400
+ definition
401
+ };
402
+ const optionsHints = [`export const ${declaration.name} = createRouteDecorator('${key.value}', { group: 'access', required: true })`];
403
+ if (extra.length > 0 || optionsNode?.type === "SpreadElement") return {
404
+ kind: "invalid",
405
+ message: `createRouteDecorator() takes a key and an optional { group?, required? } object (${declaration.name})`,
406
+ hints: optionsHints,
407
+ definition
408
+ };
409
+ let options;
410
+ if (optionsNode !== void 0) {
411
+ const evaluated = ctx.project.evaluate(scope, optionsNode, { refs: "none" });
412
+ if (evaluated.kind !== "value") return {
413
+ kind: "invalid",
414
+ message: `the options of the route decorator ${declaration.name} must be statically evaluable (literals and \`const\` references)`,
415
+ hints: optionsHints,
416
+ definition
417
+ };
418
+ options = evaluated.value;
419
+ }
420
+ const parsed = parseRouteDecoratorOptions(key.value, options);
421
+ if ("error" in parsed) return {
422
+ kind: "invalid",
423
+ message: `the options of the route decorator ${declaration.name} ${parsed.error}`,
424
+ hints: optionsHints,
425
+ definition
426
+ };
427
+ return {
428
+ kind: "route",
429
+ key: key.value,
430
+ ...parsed,
431
+ name: declaration.name,
432
+ definition
433
+ };
434
+ }
435
+ /**
436
+ * The group and `required` of a route decorator from its evaluated options (`undefined`: the
437
+ * defaults), or why the options are invalid.
438
+ */
439
+ function parseRouteDecoratorOptions(key, options) {
440
+ if (options === void 0) return {
441
+ group: key,
442
+ required: false
443
+ };
444
+ if (!isPlainObject$1(options)) return { error: "must be an object { group?, required? }" };
445
+ const unknown = Object.keys(options).find((name) => name !== "group" && name !== "required");
446
+ if (unknown !== void 0) return { error: `have an unknown property "${unknown}" (expected group and required)` };
447
+ const group = options["group"] ?? key;
448
+ if (typeof group !== "string" || group !== key && !GROUP_NAME.test(group)) return { error: `have an invalid group ${typeof group === "object" && group !== null ? "a reference" : JSON.stringify(group)}: a letter followed by letters, digits, _ . or -` };
449
+ const required = options["required"] ?? false;
450
+ if (typeof required !== "boolean") return { error: "have a non-boolean \"required\"" };
451
+ return {
452
+ group,
453
+ required
454
+ };
455
+ }
456
+ function isPlainObject$1(value) {
457
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
458
+ const prototype = Object.getPrototypeOf(value);
459
+ return prototype === Object.prototype || prototype === null;
460
+ }
461
+ function isEntityName(node) {
462
+ if (node.type === "Identifier") return true;
463
+ if (node.type === "MemberExpression" && node["computed"] !== true) {
464
+ const object = child(node, "object");
465
+ return child(node, "property")?.type === "Identifier" && object !== void 0 && isEntityName(object);
466
+ }
467
+ return false;
468
+ }
469
+ /**
470
+ * Metadata entries of the route decorators applied to a class or a method (source order).
471
+ * `target` names the class or the handler (`BookController`, `BookController.remove`). Problems are
472
+ * reported as ZNIGHT005: two decisions of one group on the same target included.
473
+ */
474
+ function routeMeta(ctx, cls, decorators, target) {
475
+ const entries = [];
476
+ for (const decorator of decorators) {
477
+ if (decorator.owner !== void 0 || decorator.identity === void 0) continue;
478
+ const resolution = resolveRouteDecorator(ctx, decorator.identity);
479
+ if (resolution.kind === "other") continue;
480
+ if (resolution.kind === "invalid") {
481
+ report(ctx, decorator, `Invalid route decorator @${decorator.name}: ${resolution.message}`, resolution.hints, resolution.definition === void 0 ? void 0 : {
482
+ message: "The route decorator is defined here",
483
+ location: resolution.definition
484
+ });
485
+ continue;
486
+ }
487
+ const value = metaValue(ctx, cls, decorator);
488
+ if (value === INVALID) continue;
489
+ const previous = entries.find((entry) => entry.group === resolution.group);
490
+ if (previous) {
491
+ if (previous.key === resolution.key && resolution.group === resolution.key) report(ctx, decorator, `@${decorator.name} sets the route metadata '${resolution.key}', already set on the same target`, ["Apply each route decorator once per controller or route"], {
492
+ message: "First applied here",
493
+ location: previous.location
494
+ });
495
+ else {
496
+ const alternatives = previous.decorator === resolution.name ? `@${resolution.name} once` : `one of @${previous.decorator}, @${resolution.name}`;
497
+ report(ctx, decorator, `@${resolution.name} sets the "${resolution.group}" decision of ${target}, already set by @${previous.decorator} on the same target`, [`Keep ${alternatives}: the decorators of the "${resolution.group}" group are alternatives`], {
498
+ message: "First applied here",
499
+ location: previous.location
500
+ });
501
+ }
502
+ continue;
503
+ }
504
+ entries.push({
505
+ key: resolution.key,
506
+ value,
507
+ location: decorator.location,
508
+ group: resolution.group,
509
+ required: resolution.required,
510
+ decorator: resolution.name,
511
+ identity: decorator.identity,
512
+ definition: resolution.definition
513
+ });
514
+ }
515
+ return entries;
516
+ }
517
+ /** `true` when the decorator is a route metadata decorator. */
518
+ function isRouteMetaDecorator(ctx, decorator) {
519
+ return decorator.owner === void 0 && decorator.identity !== void 0 && resolveRouteDecorator(ctx, decorator.identity).kind !== "other";
520
+ }
521
+ const INVALID = Symbol("invalid");
522
+ function metaValue(ctx, cls, decorator) {
523
+ if (!decorator.called) {
524
+ report(ctx, decorator, `@${decorator.name} must be called: @${decorator.name}()`, [`Write @${decorator.name}(${decorator.args.length > 0 ? "..." : ""})`]);
525
+ return INVALID;
526
+ }
527
+ const [arg, ...extra] = decorator.args;
528
+ if (arg === void 0) return true;
529
+ if (extra.length > 0) {
530
+ report(ctx, decorator, `@${decorator.name} takes at most one argument`, ["Pass a single value (an array or an object)"]);
531
+ return INVALID;
532
+ }
533
+ if (arg.text.startsWith("...")) {
534
+ report(ctx, decorator, `@${decorator.name} does not accept spread arguments`, ["Pass the value directly"]);
535
+ return INVALID;
536
+ }
537
+ const evaluation = ctx.project.evaluate(cls.file, arg.node, { refs: "nested" });
538
+ switch (evaluation.kind) {
539
+ case "value": return checkRefs(ctx, decorator, evaluation.value) ? evaluation.value : INVALID;
540
+ case "ref": return checkRefs(ctx, decorator, evaluation.ref) ? evaluation.ref : INVALID;
541
+ case "error":
542
+ ctx.report({
543
+ code: Codes.InvalidDecorator,
544
+ severity: "error",
545
+ message: `Invalid argument of @${decorator.name}: ${evaluation.error.message}`,
546
+ location: evaluation.error.location,
547
+ hints: [evaluation.error.hint ?? "Extract the value to `export const X = ...` and reference it"]
548
+ });
549
+ return INVALID;
550
+ }
551
+ }
552
+ /** Checks that every reference of a value can be imported by the generated module. */
553
+ function checkRefs(ctx, decorator, value) {
554
+ let ok = true;
555
+ const visit = (current) => {
556
+ if (isValueRef(current)) {
557
+ const route = ctx.project.importRoute(current.symbol);
558
+ if (!route.ok) {
559
+ ok = false;
560
+ ctx.report({
561
+ code: Codes.InvalidDecorator,
562
+ severity: "error",
563
+ message: `Invalid argument of @${decorator.name}: '${current.text}' cannot be imported by the generated module (${route.error.message})`,
564
+ location: current.location,
565
+ hints: route.error.hints.length > 0 ? [...route.error.hints] : ["Export it from its module"]
566
+ });
567
+ }
568
+ return;
569
+ }
570
+ if (Array.isArray(current)) for (const item of current) visit(item);
571
+ else if (typeof current === "object" && current !== null) for (const item of Object.values(current)) visit(item);
572
+ };
573
+ visit(value);
574
+ return ok;
575
+ }
576
+ /** `true` for a {@link ValueRef} (the only non-plain object a static value can hold). */
577
+ function isValueRef(value) {
578
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
579
+ const prototype = Object.getPrototypeOf(value);
580
+ return prototype !== Object.prototype && prototype !== null && value.kind === "ref";
581
+ }
582
+ function report(ctx, decorator, message, hints, related) {
583
+ ctx.report({
584
+ code: Codes.InvalidDecorator,
585
+ severity: "error",
586
+ message,
587
+ location: decorator.location,
588
+ hints,
589
+ ...related?.location ? { related: [{
590
+ message: related.message,
591
+ location: related.location
592
+ }] } : {}
593
+ });
594
+ }
595
+ // src/compiler/analyze.ts
596
+ /** Name of the extension (owner of the decorators in `DecoratorInfo.owner`). */
597
+ const EXTENSION_NAME = "http";
598
+ const SCOPES = [
599
+ "singleton",
600
+ "request",
601
+ "transient"
602
+ ];
603
+ /** RFC 9110 token (header names). */
604
+ const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
605
+ /** Types an upload parameter may have (`File`, `Blob`, arrays of them for `@UploadedFiles`); `true`: cast needed. */
606
+ const FILE_TYPES = /* @__PURE__ */ new Map([
607
+ ["globalThis#File", false],
608
+ ["globalThis#Blob", false],
609
+ ["node:buffer#File", true],
610
+ ["node:buffer#Blob", true]
611
+ ]);
612
+ /** Methods whose requests usually have no body (upload parameters there are a warning). */
613
+ const BODYLESS_METHODS = /* @__PURE__ */ new Set([
614
+ "GET",
615
+ "HEAD",
616
+ "DELETE",
617
+ "OPTIONS"
618
+ ]);
619
+ const UPLOAD_OPTIONS = [
620
+ "required",
621
+ "maxSize",
622
+ "maxFiles",
623
+ "mimeTypes"
624
+ ];
625
+ /** Smallest `@Sse` heartbeat and `maxDuration` (ms). */
626
+ const MIN_SSE_INTERVAL_MS = 1e3;
627
+ /** Longest `@Sse` heartbeat and `maxDuration` (ms): the longest `setTimeout` delay. */
628
+ const MAX_SSE_TIMER_MS = 2147483647;
629
+ /** Primitive return types an `@Sse` handler cannot return (`any`, `unknown`, `object`, `never` are accepted). */
630
+ const NOT_SSE_SOURCES = /* @__PURE__ */ new Set([
631
+ "string",
632
+ "number",
633
+ "boolean",
634
+ "bigint",
635
+ "symbol",
636
+ "undefined",
637
+ "null",
638
+ "void"
639
+ ]);
640
+ /** Values that are fine without schema (`@Body() body: string`, `@Body() bytes: Uint8Array`...). */
641
+ const UNVALIDATED_REFERENCES = /* @__PURE__ */ new Set([
642
+ "globalThis#Uint8Array",
643
+ "globalThis#ArrayBuffer",
644
+ "globalThis#Buffer",
645
+ "globalThis#Blob",
646
+ "globalThis#FormData",
647
+ "node:buffer#Buffer",
648
+ "@znight/http#QueryRecord"
649
+ ]);
650
+ /** Analyses a class carrying at least one `@znight/http` decorator. */
651
+ function analyzeHttpClass(cls, ctx) {
652
+ const controllers = cls.decorators.filter((decorator) => decorator.identity === Ids.Controller);
653
+ const [controller] = controllers;
654
+ if (controller === void 0) {
655
+ reportOutsideController(cls, ctx);
656
+ return;
657
+ }
658
+ for (const duplicate of controllers.slice(1)) error(ctx, duplicate.location, `@Controller is applied several times to ${cls.displayName}`, ["Keep a single @Controller()"]);
659
+ const options = controllerOptions(ctx, controller);
660
+ const token = ctx.registerProvider(cls, { scope: options.scope });
661
+ const basePath = normalizePath(options.path);
662
+ const guards = classRefs(ctx, cls, cls.decorators, Ids.UseGuard, "guard");
663
+ const interceptors = classRefs(ctx, cls, cls.decorators, Ids.UseInterceptor, "interceptor");
664
+ const meta = routeMeta(ctx, cls, cls.decorators, cls.displayName);
665
+ const bodyLimit = bodyLimitOf(ctx, cls.decorators);
666
+ const routes = [];
667
+ for (const method of cls.methods) routes.push(...analyzeMethod(ctx, cls, method, basePath, bodyLimit));
668
+ ctx.store({
669
+ token,
670
+ identity: cls.identity,
671
+ displayName: cls.displayName,
672
+ rawBasePath: options.path,
673
+ basePath,
674
+ guards,
675
+ interceptors,
676
+ meta,
677
+ routes,
678
+ location: controller.location,
679
+ pathLocation: options.pathLocation
680
+ });
681
+ }
682
+ function controllerOptions(ctx, decorator) {
683
+ const [arg] = decorator.args;
684
+ const defaults = {
685
+ path: "",
686
+ pathLocation: decorator.location,
687
+ scope: "singleton"
688
+ };
689
+ if (arg === void 0 || !arg.evaluated) return defaults;
690
+ const value = arg.value;
691
+ if (typeof value === "string") return {
692
+ ...defaults,
693
+ path: value,
694
+ pathLocation: arg.location
695
+ };
696
+ if (!isPlainObject(value)) {
697
+ error(ctx, arg.location, `@Controller expects a path or { path?, scope? }, got ${describe(value)}`, ["@Controller('/users') or @Controller({ path: '/users', scope: 'request' })"]);
698
+ return defaults;
699
+ }
700
+ let { path, scope } = defaults;
701
+ for (const [key, option] of Object.entries(value)) if (key === "path") {
702
+ if (typeof option === "string") path = option;
703
+ else if (option !== void 0) error(ctx, arg.location, `@Controller: "path" must be a string, got ${describe(option)}`);
704
+ } else if (key === "scope") {
705
+ if (SCOPES.includes(option)) scope = option;
706
+ else if (option !== void 0) error(ctx, arg.location, `@Controller: invalid scope ${describe(option)}`, [`Use one of ${SCOPES.map((candidate) => `'${candidate}'`).join(", ")}`]);
707
+ } else error(ctx, arg.location, `@Controller: unknown option "${key}"`, ["Options: { path?, scope? }"]);
708
+ return {
709
+ path,
710
+ pathLocation: arg.location,
711
+ scope
712
+ };
713
+ }
714
+ /**
715
+ * HTTP decorators on a class that is not a controller have no effect. On a class another extension
716
+ * owns (a `@Consumer` of `@znight/messaging`...), adding `@Controller()` is no fix: a class is the
717
+ * provider of a single extension.
718
+ */
719
+ function reportOutsideController(cls, ctx) {
720
+ const owner = foreignOwner(cls);
721
+ const hint = owner === void 0 ? [`Add @Controller() to ${cls.displayName} (routes are not inherited from base classes)`] : [`${cls.displayName} is a @${owner.name} class of '${owner.module}': HTTP decorators only apply to @Controller() classes`];
722
+ const decorators = [...cls.decorators];
723
+ for (const method of cls.methods) {
724
+ decorators.push(...method.decorators);
725
+ const handler = owner === void 0 ? void 0 : method.decorators.find((decorator) => decorator.owner === owner.decorator.owner);
726
+ if (owner !== void 0 && handler !== void 0) {
727
+ reportHandlerParams(ctx, cls, method, owner, handler);
728
+ continue;
729
+ }
730
+ for (const param of method.params) decorators.push(...unique(param.decorators));
731
+ }
732
+ for (const decorator of decorators) if (decorator.owner === "http" && validTarget(decorator)) error(ctx, decorator.location, `@${decorator.name} has no effect: ${cls.displayName} is not a controller`, hint);
733
+ }
734
+ /**
735
+ * HTTP decorators on the parameters of a handler of the extension owning the class (an `@OnMessage`
736
+ * handler...). That extension reads its handlers' parameters and reports those it cannot read
737
+ * (`Headers` imported from `@znight/http` instead of `@znight/messaging`...): an HTTP decorator is
738
+ * only reported here when the parameter is read through a decorator of that extension, or is
739
+ * optional (left undefined without a word).
740
+ */
741
+ function reportHandlerParams(ctx, cls, method, owner, handler) {
742
+ for (const param of method.params) {
743
+ if (!param.decorators.some((decorator) => decorator.owner === owner.decorator.owner) && !param.optional && !param.hasDefault && !param.rest) continue;
744
+ for (const decorator of unique(param.decorators)) if (decorator.owner === "http" && validTarget(decorator)) error(ctx, decorator.location, `@${decorator.name} has no effect on parameter '${param.name}' of ${cls.displayName}.${method.name}: it is an @${handler.name} handler of '${owner.module}'`, [`Use the parameter decorators of '${owner.module}', or remove it`]);
745
+ }
746
+ }
747
+ function qualify(decorator) {
748
+ const identity = decorator.identity;
749
+ const separator = identity?.lastIndexOf("#") ?? -1;
750
+ if (identity === void 0 || separator <= 0) return;
751
+ return {
752
+ decorator,
753
+ module: identity.slice(0, separator),
754
+ name: identity.slice(separator + 1)
755
+ };
756
+ }
757
+ /** The class decorator through which another extension owns the class (`@Consumer`...). */
758
+ function foreignOwner(cls) {
759
+ for (const decorator of cls.decorators) {
760
+ const owner = decorator.owner;
761
+ if (owner === void 0 || owner === "http" || owner === "core" || owner === "manifest") continue;
762
+ const qualified = qualify(decorator);
763
+ if (qualified !== void 0) return qualified;
764
+ }
765
+ }
766
+ /**
767
+ * A parameter decorator named like an HTTP one but imported from another package (`Headers` and
768
+ * `Ctx` of `@znight/messaging`, typically auto-imported).
769
+ */
770
+ function foreignParameterDecorator(param) {
771
+ for (const decorator of unique(param.decorators)) {
772
+ const qualified = qualify(decorator);
773
+ if (qualified !== void 0 && qualified.module !== "@znight/http" && PARAM_SOURCES.has(`@znight/http#${qualified.name}`)) return qualified;
774
+ }
775
+ }
776
+ function analyzeMethod(ctx, cls, method, basePath, controllerBodyLimit) {
777
+ const where = `${cls.displayName}.${method.name}`;
778
+ let routeDecorators = method.decorators.filter((decorator) => ROUTE_METHODS.has(decorator.identity ?? ""));
779
+ if (routeDecorators.length === 0) {
780
+ reportNotARoute(ctx, method, where);
781
+ return [];
782
+ }
783
+ if (!checkHandler(ctx, method, where, routeDecorators[0])) return [];
784
+ const sseDecorator = routeDecorators.find((decorator) => decorator.identity === Ids.Sse);
785
+ if (sseDecorator !== void 0) {
786
+ for (const other of routeDecorators) {
787
+ if (other === sseDecorator) continue;
788
+ error(ctx, other.location, other.identity === Ids.Sse ? `@Sse is applied several times to ${where}` : `@${other.name} cannot be combined with @Sse on ${where}`, ["One handler per event stream: declare other routes on other methods (HttpResponse.sse() serves an event stream from any route)"]);
789
+ }
790
+ routeDecorators = [sseDecorator];
791
+ }
792
+ const decorators = method.decorators;
793
+ const bodyValidation = validateSchema(ctx, decorators);
794
+ const { params, complete } = analyzeParams(ctx, cls, method, where, bodyValidation);
795
+ let status = httpCode(ctx, decorators);
796
+ let headers = responseHeaders(ctx, decorators);
797
+ const guards = classRefs(ctx, cls, decorators, Ids.UseGuard, "guard");
798
+ const interceptors = classRefs(ctx, cls, decorators, Ids.UseInterceptor, "interceptor");
799
+ const meta = routeMeta(ctx, cls, decorators, where);
800
+ const bodySchema = bodyValidation !== void 0 && !params.some((param) => param.source === "body") ? bodyValidation.schema : void 0;
801
+ let rawResponse = params.some((param) => param.source === "response");
802
+ const bodyLimit = bodyLimitOf(ctx, decorators) ?? controllerBodyLimit;
803
+ const uploads = checkUploads(ctx, params, where);
804
+ if (sseDecorator !== void 0) {
805
+ checkSseHandler(ctx, method, params, where, bodyValidation);
806
+ status = void 0;
807
+ headers = headers.filter(([name]) => name !== "content-type");
808
+ rawResponse = false;
809
+ }
810
+ const routes = [];
811
+ for (const decorator of routeDecorators) {
812
+ const httpMethod = ROUTE_METHODS.get(decorator.identity ?? "");
813
+ const target = decorator === sseDecorator ? sseArgs(ctx, decorator) : routePath(ctx, decorator);
814
+ if (target === void 0) continue;
815
+ const { rawPath, arg } = target;
816
+ const path = normalizePath(rawPath);
817
+ if (uploads && BODYLESS_METHODS.has(httpMethod)) ctx.report({
818
+ code: Codes.InvalidDecorator,
819
+ severity: "warning",
820
+ message: `${where} reads uploaded files but is a ${httpMethod} route: ${httpMethod} requests usually have no body`,
821
+ location: decorator.location,
822
+ hints: ["Upload files with @Post(), @Put() or @Patch()"]
823
+ });
824
+ routes.push({
825
+ method: httpMethod,
826
+ rawPath,
827
+ path,
828
+ fullPath: joinPaths(basePath, path),
829
+ handler: method.name,
830
+ status,
831
+ headers,
832
+ meta,
833
+ guards,
834
+ interceptors,
835
+ params,
836
+ complete,
837
+ bodySchema,
838
+ bodyLimit,
839
+ rawResponse,
840
+ ...target.sse === void 0 ? {} : { sse: target.sse },
841
+ location: decorator.location,
842
+ pathLocation: arg?.location ?? decorator.location
843
+ });
844
+ }
845
+ return routes;
846
+ }
847
+ /** The path argument of a route decorator (`undefined` after an error). */
848
+ function routePath(ctx, decorator) {
849
+ const [arg] = decorator.args;
850
+ if (arg === void 0) return {
851
+ rawPath: "",
852
+ arg
853
+ };
854
+ if (!arg.evaluated) return;
855
+ if (typeof arg.value !== "string") {
856
+ error(ctx, arg.location, `@${decorator.name} expects a path (string), got ${describe(arg.value)}`, [`@${decorator.name}('/:id')`]);
857
+ return;
858
+ }
859
+ return {
860
+ rawPath: arg.value,
861
+ arg
862
+ };
863
+ }
864
+ /** `@Sse(path?, options?)` / `@Sse(options)`: the path and the options (`undefined` after an error). */
865
+ function sseArgs(ctx, decorator) {
866
+ const [first, second] = decorator.args;
867
+ let pathArg;
868
+ let options;
869
+ if (first !== void 0) {
870
+ if (!first.evaluated) return;
871
+ if (typeof first.value === "string") {
872
+ pathArg = first;
873
+ options = second;
874
+ } else if (first.value === void 0) options = second;
875
+ else if (isPlainObject(first.value)) {
876
+ if (second !== void 0) {
877
+ error(ctx, second.location, "@Sse(options) takes a single argument", ["Put the path first: @Sse('/events', { retry: 5000 })"]);
878
+ return;
879
+ }
880
+ options = first;
881
+ } else {
882
+ error(ctx, first.location, `@Sse expects a path (string) or options, got ${describe(first.value)}`, ["@Sse('/events'), @Sse('/events', { heartbeat: 15_000 }) or @Sse({ retry: 5000 })"]);
883
+ return;
884
+ }
885
+ }
886
+ const sse = sseOptions(ctx, options);
887
+ if (sse === void 0) return;
888
+ return {
889
+ rawPath: typeof pathArg?.value === "string" ? pathArg.value : "",
890
+ arg: pathArg,
891
+ sse
892
+ };
893
+ }
894
+ /** The static `{ heartbeat?, retry?, maxDuration? }` of `@Sse` (`undefined` after an error). */
895
+ function sseOptions(ctx, arg) {
896
+ if (arg === void 0) return {};
897
+ if (!arg.evaluated) return;
898
+ const shape = "{ heartbeat?: number | false, retry?: number, maxDuration?: number }";
899
+ if (arg.value === void 0) return {};
900
+ if (!isPlainObject(arg.value)) {
901
+ error(ctx, arg.location, `@Sse options must be an object ${shape}, got ${describe(arg.value)}`);
902
+ return;
903
+ }
904
+ const result = {};
905
+ let ok = true;
906
+ const invalid = (message, hints = []) => {
907
+ ok = false;
908
+ error(ctx, arg.location, `@Sse: ${message}`, hints);
909
+ };
910
+ const integer = (value, min, max = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max;
911
+ for (const [key, value] of Object.entries(arg.value)) {
912
+ if (value === void 0) continue;
913
+ switch (key) {
914
+ case "heartbeat":
915
+ if (value === false) result.heartbeat = 0;
916
+ else if (integer(value, MIN_SSE_INTERVAL_MS, MAX_SSE_TIMER_MS)) result.heartbeat = value;
917
+ else invalid(`"heartbeat" must be an integer number of ms from ${MIN_SSE_INTERVAL_MS} to ${MAX_SSE_TIMER_MS}, or false, got ${describe(value)}`, ["Keep it below the idle timeouts of the proxies and of Bun.serve() (10 s): the default is 8 000"]);
918
+ break;
919
+ case "retry":
920
+ if (integer(value, 0)) result.retry = value;
921
+ else invalid(`"retry" must be an integer number of ms ≥ 0, got ${describe(value)}`);
922
+ break;
923
+ case "maxDuration":
924
+ if (integer(value, MIN_SSE_INTERVAL_MS, MAX_SSE_TIMER_MS)) result.maxDuration = value;
925
+ else invalid(`"maxDuration" must be an integer number of ms from ${MIN_SSE_INTERVAL_MS} to ${MAX_SSE_TIMER_MS} (about 24.8 days), got ${describe(value)}`);
926
+ break;
927
+ default: invalid(`unknown option "${key}"`, [`Options: ${shape}`]);
928
+ }
929
+ }
930
+ return ok ? result : void 0;
931
+ }
932
+ /**
933
+ * `@Sse` handlers: the plugin writes the stream with status 200 and its own content type, so
934
+ * `@HttpCode`, `@Header('content-type')` and `@Res()` are errors; a body is a warning (EventSource
935
+ * sends `GET` without body); a primitive return type is an error.
936
+ */
937
+ function checkSseHandler(ctx, method, params, where, bodyValidation) {
938
+ for (const decorator of method.decorators) if (decorator.identity === Ids.HttpCode) error(ctx, decorator.location, `@HttpCode has no effect on @Sse route ${where}: an event stream answers 200`, ["Throw an HttpException or return an HttpResponse from the handler for other statuses"]);
939
+ else if (decorator.identity === Ids.Header) {
940
+ const [name] = decorator.args;
941
+ if (name?.evaluated === true && typeof name.value === "string" && name.value.toLowerCase() === "content-type") error(ctx, decorator.location, `@Header('content-type') has no effect on @Sse route ${where}`, ["An event stream is always sent as text/event-stream; charset=utf-8"]);
942
+ }
943
+ for (const param of params) if (param.source === "response") error(ctx, param.location, `@Res() cannot be used on @Sse route ${where}: the plugin writes the event stream`, ["Return an AsyncIterable of SseEvent, or use a plain route to write the response yourself"]);
944
+ const bodyLocation = params.find((param) => param.source === "body" || param.upload !== void 0)?.location ?? bodyValidation?.decorator.location;
945
+ if (bodyLocation !== void 0) ctx.report({
946
+ code: Codes.InvalidDecorator,
947
+ severity: "warning",
948
+ message: `@Sse route ${where} reads a request body: EventSource sends GET requests without body`,
949
+ location: bodyLocation,
950
+ hints: ["For fetch() clients streaming a POST, use @Post() and return HttpResponse.sse(source)"]
951
+ });
952
+ const returned = method.returnType === void 0 ? void 0 : ctx.types.awaited(method.returnType);
953
+ if (returned !== void 0 && !method.generator) {
954
+ if ((returned.kind === "union" ? returned.types : [returned]).some((member) => member.kind === "literal" || member.kind === "primitive" && NOT_SSE_SOURCES.has(member.name))) error(ctx, method.location, `@Sse handler ${where} returns ${ctx.types.format(returned)}: return an AsyncIterable, an Iterable or a ReadableStream of SseEvent (or an HttpResponse)`, ["async events(): Promise<AsyncIterable<SseEvent<T>>> { ...; return channel }"]);
955
+ }
956
+ }
957
+ /** `@BodyLimit(size)` of a class or method, in bytes. */
958
+ function bodyLimitOf(ctx, decorators) {
959
+ const [decorator, ...duplicates] = decorators.filter((candidate) => candidate.identity === Ids.BodyLimit);
960
+ for (const duplicate of duplicates) error(ctx, duplicate.location, "@BodyLimit is applied several times", ["Keep a single @BodyLimit(size)"]);
961
+ if (decorator === void 0) return;
962
+ const [arg] = decorator.args;
963
+ if (arg === void 0) {
964
+ error(ctx, decorator.location, "@BodyLimit requires a size", ["@BodyLimit('10mb') or @BodyLimit(10_485_760)"]);
965
+ return;
966
+ }
967
+ if (!arg.evaluated) return;
968
+ const size = parseByteSize(arg.value);
969
+ if (size === void 0) error(ctx, arg.location, `@BodyLimit expects a size in bytes, got ${describe(arg.value)}`, ["A non-negative integer (10_485_760), a size with a unit ('10mb', '512kb'; kb = 1024 bytes) or Infinity"]);
970
+ return size;
971
+ }
972
+ /**
973
+ * Checks the upload parameters of a handler together (one parameter per field; a parameter without
974
+ * field takes every file and stands alone). Returns whether the handler reads uploaded files.
975
+ */
976
+ function checkUploads(ctx, params, where) {
977
+ const uploads = params.filter((param) => param.upload !== void 0);
978
+ uploads.forEach((param, index) => {
979
+ const field = param.upload?.field;
980
+ const previous = uploads.slice(0, index).find((other) => field === void 0 || other.upload?.field === void 0 || other.upload.field === field);
981
+ if (previous === void 0) return;
982
+ ctx.report({
983
+ code: Codes.InvalidDecorator,
984
+ severity: "error",
985
+ message: field !== void 0 && previous.upload?.field === field ? `The field "${field}" of ${where} is read by several upload parameters` : `${where}: an upload parameter without field receives every file of the body and cannot be combined with other upload parameters`,
986
+ location: param.location,
987
+ hints: ["Give each @UploadedFile / @UploadedFiles parameter its own field"],
988
+ related: [{
989
+ message: "Other upload parameter",
990
+ location: previous.location
991
+ }]
992
+ });
993
+ });
994
+ return uploads.length > 0;
995
+ }
996
+ /** HTTP method decorators (and parameter decorators) on a method without route decorator. */
997
+ function reportNotARoute(ctx, method, where) {
998
+ const hint = [`Add a route decorator to ${where}: @Get(), @Post(), @Put(), @Patch(), @Delete()...`];
999
+ for (const decorator of method.decorators) if (decorator.owner === "http" && validTarget(decorator)) error(ctx, decorator.location, `@${decorator.name} has no effect: ${where} is not a route`, hint);
1000
+ else if (isRouteMetaDecorator(ctx, decorator)) ctx.report({
1001
+ code: Codes.InvalidDecorator,
1002
+ severity: "warning",
1003
+ message: `@${decorator.name} has no effect: ${where} is not a route`,
1004
+ location: decorator.location,
1005
+ hints: hint
1006
+ });
1007
+ for (const param of method.params) for (const decorator of unique(param.decorators)) if (decorator.owner === "http" && validTarget(decorator)) error(ctx, decorator.location, `@${decorator.name} has no effect: ${where} is not a route`, hint);
1008
+ }
1009
+ /** A route handler must be a public, non-static method with a plain name. */
1010
+ function checkHandler(ctx, method, where, decorator) {
1011
+ let problem;
1012
+ let hint = "Route handlers are public instance methods";
1013
+ if (method.static) problem = `Static method ${where} cannot be a route handler`;
1014
+ else if (method.privateName) {
1015
+ problem = `#private method ${where} cannot be a route handler`;
1016
+ hint = "Use a public method (the generated code calls it from outside the class)";
1017
+ } else if (method.visibility !== "public") {
1018
+ problem = `${method.visibility === "protected" ? "Protected" : "Private"} method ${where} cannot be a route handler`;
1019
+ hint = "Make the method public (the generated code calls it from outside the class)";
1020
+ } else if (method.kind !== "method") problem = `Accessor ${where} cannot be a route handler`;
1021
+ else if (method.computed) problem = `${where} cannot be a route handler: handlers need a plain name (no computed key)`;
1022
+ if (problem !== void 0) {
1023
+ error(ctx, decorator.location, problem, [hint]);
1024
+ return false;
1025
+ }
1026
+ return true;
1027
+ }
1028
+ function httpCode(ctx, decorators) {
1029
+ const [decorator, ...duplicates] = decorators.filter((candidate) => candidate.identity === Ids.HttpCode);
1030
+ for (const duplicate of duplicates) error(ctx, duplicate.location, "@HttpCode is applied several times", ["Keep a single @HttpCode(status)"]);
1031
+ if (decorator === void 0) return;
1032
+ const [arg] = decorator.args;
1033
+ if (arg === void 0) {
1034
+ error(ctx, decorator.location, "@HttpCode requires a status", ["@HttpCode(202)"]);
1035
+ return;
1036
+ }
1037
+ if (!arg.evaluated) return;
1038
+ const status = arg.value;
1039
+ if (typeof status !== "number" || !Number.isInteger(status) || status < 100 || status > 599) {
1040
+ error(ctx, arg.location, `@HttpCode expects an integer status between 100 and 599, got ${describe(status)}`);
1041
+ return;
1042
+ }
1043
+ return status;
1044
+ }
1045
+ function responseHeaders(ctx, decorators) {
1046
+ const headers = [];
1047
+ const seen = /* @__PURE__ */ new Map();
1048
+ for (const decorator of decorators) {
1049
+ if (decorator.identity !== Ids.Header) continue;
1050
+ const [name, value] = decorator.args;
1051
+ if (name === void 0 || value === void 0) {
1052
+ error(ctx, decorator.location, "@Header requires a name and a value", ["@Header('cache-control', 'no-store')"]);
1053
+ continue;
1054
+ }
1055
+ if (!name.evaluated || !value.evaluated) continue;
1056
+ if (typeof name.value !== "string" || !TOKEN.test(name.value)) {
1057
+ error(ctx, name.location, `@Header: invalid header name ${describe(name.value)}`, ["Header names are tokens: letters, digits and ! # $ % & ' * + - . ^ _ ` | ~"]);
1058
+ continue;
1059
+ }
1060
+ if (typeof value.value !== "string" || /[\r\n\0]/.test(value.value)) {
1061
+ error(ctx, value.location, `@Header: the value of "${name.value}" must be a string without line breaks`);
1062
+ continue;
1063
+ }
1064
+ const lower = name.value.toLowerCase();
1065
+ const previous = seen.get(lower);
1066
+ if (previous) {
1067
+ ctx.report({
1068
+ code: Codes.InvalidDecorator,
1069
+ severity: "error",
1070
+ message: `@Header: "${lower}" is set several times`,
1071
+ location: decorator.location,
1072
+ hints: ["Set each response header once (or use ctx.appendHeader() in the handler)"],
1073
+ related: [{
1074
+ message: "First set here",
1075
+ location: previous
1076
+ }]
1077
+ });
1078
+ continue;
1079
+ }
1080
+ seen.set(lower, decorator.location);
1081
+ headers.push([lower, value.value]);
1082
+ }
1083
+ return headers;
1084
+ }
1085
+ /** `@Validate(schema)` of a handler. */
1086
+ function validateSchema(ctx, decorators) {
1087
+ const [decorator, ...duplicates] = decorators.filter((candidate) => candidate.identity === Ids.Validate);
1088
+ for (const duplicate of duplicates) error(ctx, duplicate.location, "@Validate is applied several times", ["Keep a single @Validate(schema): compose the validation in the schema"]);
1089
+ if (decorator === void 0) return;
1090
+ const [arg] = decorator.args;
1091
+ if (arg === void 0) {
1092
+ error(ctx, decorator.location, "@Validate requires a schema", ["@Validate(CreateUser)"]);
1093
+ return;
1094
+ }
1095
+ const schema = schemaRef(ctx, arg, "@Validate");
1096
+ return schema ? {
1097
+ schema,
1098
+ decorator
1099
+ } : void 0;
1100
+ }
1101
+ /** Guard / interceptor references of a class or method, bound to provider tokens (in order). */
1102
+ function classRefs(ctx, cls, decorators, identity, role) {
1103
+ const refs = [];
1104
+ for (const decorator of decorators) {
1105
+ if (decorator.identity !== identity) continue;
1106
+ if (decorator.called && decorator.args.length === 0) error(ctx, decorator.location, `@${decorator.name} requires at least one ${role}`, [`@${decorator.name}(My${role === "guard" ? "Guard" : "Interceptor"})`]);
1107
+ for (const arg of decorator.args) {
1108
+ const ref = ctx.resolveClassRef(arg, {
1109
+ from: cls,
1110
+ role
1111
+ });
1112
+ if (ref) refs.push(ref);
1113
+ }
1114
+ }
1115
+ return refs;
1116
+ }
1117
+ function analyzeParams(ctx, cls, method, where, bodyValidation) {
1118
+ const params = [];
1119
+ let complete = true;
1120
+ for (const param of method.params) {
1121
+ const [decorator, ...extra] = unique(param.decorators).filter((decorator) => decorator.owner === "http" && PARAM_SOURCES.has(decorator.identity ?? ""));
1122
+ const foreign = decorator === void 0 ? foreignParameterDecorator(param) : void 0;
1123
+ if (foreign !== void 0) {
1124
+ error(ctx, foreign.decorator.location, `@${foreign.decorator.name} on parameter '${param.name}' of ${where} is imported from '${foreign.module}': route parameters use ${foreign.name} from '${HTTP_PACKAGE}'`, [`Import it from '${HTTP_PACKAGE}': import { ${foreign.name} } from '${HTTP_PACKAGE}'`]);
1125
+ complete = false;
1126
+ continue;
1127
+ }
1128
+ if (decorator === void 0) {
1129
+ error(ctx, param.location, `Parameter '${param.name}' of ${where} has no HTTP decorator`, ["Decorate it with @Param(), @Query(), @Body(), @Headers(), @Cookie(), @SignedCookie(), @UploadedFile(), @UploadedFiles(), @Ctx(), @Req() or @Res()"]);
1130
+ complete = false;
1131
+ continue;
1132
+ }
1133
+ for (const other of extra) error(ctx, other.location, `Parameter '${param.name}' of ${where} has several HTTP decorators`, ["Keep a single parameter decorator"]);
1134
+ if (param.rest) {
1135
+ error(ctx, decorator.location, `Rest parameter '${param.name}' of ${where} cannot be injected`);
1136
+ complete = false;
1137
+ continue;
1138
+ }
1139
+ const data = analyzeParam(ctx, cls, method, param, decorator, where, bodyValidation);
1140
+ if (data) params.push(data);
1141
+ else complete = false;
1142
+ }
1143
+ return {
1144
+ params,
1145
+ complete
1146
+ };
1147
+ }
1148
+ function analyzeParam(ctx, cls, method, param, decorator, where, bodyValidation) {
1149
+ const source = PARAM_SOURCES.get(decorator.identity ?? "");
1150
+ if (source === void 0) return;
1151
+ const declared = parameterType(ctx, cls, method, param);
1152
+ const defaultValue = staticDefault(param);
1153
+ const base = {
1154
+ index: param.index,
1155
+ name: param.name,
1156
+ source,
1157
+ type: void 0,
1158
+ ...defaultValue === void 0 ? {} : { defaultValue: defaultValue.value },
1159
+ location: decorator.location
1160
+ };
1161
+ const type = declared.type === void 0 ? void 0 : ctx.types.format(declared.type);
1162
+ const plain = () => ({
1163
+ ...base,
1164
+ key: void 0,
1165
+ optional: false,
1166
+ schema: void 0,
1167
+ coercion: void 0
1168
+ });
1169
+ switch (source) {
1170
+ case "request":
1171
+ case "context": return plain();
1172
+ case "response":
1173
+ ctx.report({
1174
+ code: Codes.InvalidDecorator,
1175
+ severity: "warning",
1176
+ message: `@Res() makes ${where} adapter-specific: the handler writes the native response itself`,
1177
+ location: decorator.location,
1178
+ hints: ["Prefer returning a value, an HttpResponse or a Web Response (portable across adapters)"]
1179
+ });
1180
+ return plain();
1181
+ case "body": {
1182
+ const [arg] = decorator.args;
1183
+ let schema;
1184
+ if (arg !== void 0) {
1185
+ schema = schemaRef(ctx, arg, "@Body");
1186
+ if (schema === void 0) return;
1187
+ }
1188
+ if (bodyValidation !== void 0) {
1189
+ if (schema !== void 0) {
1190
+ ctx.report({
1191
+ code: Codes.InvalidDecorator,
1192
+ severity: "error",
1193
+ message: `The body of ${where} is validated twice: @Validate(${bodyValidation.schema.text}) and @Body(${schema.text})`,
1194
+ location: bodyValidation.decorator.location,
1195
+ hints: ["Keep one of them: @Validate(S) on the method is the same as @Body(S) on the parameter"],
1196
+ related: [{
1197
+ message: "@Body schema",
1198
+ location: decorator.location
1199
+ }]
1200
+ });
1201
+ return;
1202
+ }
1203
+ schema = bodyValidation.schema;
1204
+ }
1205
+ if (schema === void 0) checkUnvalidated(ctx, declared.type, decorator, param, where, "body");
1206
+ return {
1207
+ ...base,
1208
+ key: void 0,
1209
+ optional: declared.optional,
1210
+ schema,
1211
+ coercion: void 0
1212
+ };
1213
+ }
1214
+ case "file":
1215
+ case "files": {
1216
+ const upload = uploadOf(ctx, decorator, param, declared, where, source === "files");
1217
+ return upload === void 0 ? void 0 : {
1218
+ ...base,
1219
+ key: upload.field,
1220
+ optional: !upload.required,
1221
+ schema: void 0,
1222
+ coercion: void 0,
1223
+ upload
1224
+ };
1225
+ }
1226
+ case "headers": {
1227
+ const [arg] = decorator.args;
1228
+ if (arg === void 0) return plain();
1229
+ if (!arg.evaluated) return;
1230
+ if (typeof arg.value !== "string" || !TOKEN.test(arg.value)) {
1231
+ error(ctx, arg.location, `@Headers: invalid header name ${describe(arg.value)}`, ["@Headers('x-api-key'), or @Headers() for every header"]);
1232
+ return;
1233
+ }
1234
+ const key = arg.value.toLowerCase();
1235
+ const coercion = coercionOf(ctx, declared, param, decorator, where, "headers", key);
1236
+ return coercion ? {
1237
+ ...base,
1238
+ key,
1239
+ type,
1240
+ optional: declared.optional,
1241
+ schema: void 0,
1242
+ coercion
1243
+ } : void 0;
1244
+ }
1245
+ case "param":
1246
+ case "query": {
1247
+ const parsed = nameAndSchema(ctx, decorator);
1248
+ if (!parsed.ok) return;
1249
+ const { key, schema } = parsed;
1250
+ const optional = source === "query" && declared.optional;
1251
+ if (schema !== void 0) return {
1252
+ ...base,
1253
+ key,
1254
+ optional,
1255
+ schema,
1256
+ coercion: void 0
1257
+ };
1258
+ if (key === void 0) {
1259
+ if (source === "query") checkUnvalidated(ctx, declared.type, decorator, param, where, "query");
1260
+ return {
1261
+ ...base,
1262
+ key,
1263
+ optional: false,
1264
+ schema: void 0,
1265
+ coercion: void 0
1266
+ };
1267
+ }
1268
+ const coercion = coercionOf(ctx, declared, param, decorator, where, source, key);
1269
+ return coercion ? {
1270
+ ...base,
1271
+ key,
1272
+ type,
1273
+ optional,
1274
+ schema: void 0,
1275
+ coercion
1276
+ } : void 0;
1277
+ }
1278
+ case "cookie":
1279
+ case "signedCookie": return cookieParam(ctx, param, decorator, declared, where, base, type);
1280
+ }
1281
+ }
1282
+ /** `@Cookie(name?, schema?)` / `@SignedCookie(name, schema?)`. */
1283
+ function cookieParam(ctx, param, decorator, declared, where, base, type) {
1284
+ const parsed = nameAndSchema(ctx, decorator);
1285
+ if (!parsed.ok) return;
1286
+ const { key, schema } = parsed;
1287
+ const name = `@${decorator.name}`;
1288
+ if (key !== void 0 && !TOKEN.test(key)) {
1289
+ error(ctx, decorator.args[0]?.location ?? decorator.location, `${name}: invalid cookie name ${describe(key)}`, [`Cookie names are tokens (letters, digits and ! # $ % & ' * + - . ^ _ \` | ~): ${name}('session_id')`]);
1290
+ return;
1291
+ }
1292
+ if (decorator.identity === Ids.SignedCookie && key === void 0) {
1293
+ error(ctx, decorator.location, "@SignedCookie requires a cookie name", ["@SignedCookie('session_id')"]);
1294
+ return;
1295
+ }
1296
+ const optional = declared.optional;
1297
+ if (schema !== void 0) return {
1298
+ ...base,
1299
+ key,
1300
+ optional,
1301
+ schema,
1302
+ coercion: void 0
1303
+ };
1304
+ if (key === void 0) {
1305
+ checkUnvalidated(ctx, declared.type, decorator, param, where, "cookies");
1306
+ return {
1307
+ ...base,
1308
+ key,
1309
+ optional: false,
1310
+ schema: void 0,
1311
+ coercion: void 0
1312
+ };
1313
+ }
1314
+ const coercion = coercionOf(ctx, declared, param, decorator, where, "cookie", key);
1315
+ return coercion ? {
1316
+ ...base,
1317
+ key,
1318
+ type,
1319
+ optional,
1320
+ schema: void 0,
1321
+ coercion
1322
+ } : void 0;
1323
+ }
1324
+ /**
1325
+ * The statically evaluable default value of a parameter (`page = 1`), without references to
1326
+ * exported bindings. `ParameterInfo.initializer` is read defensively (compilers before RFC 0002 C4
1327
+ * do not provide it).
1328
+ */
1329
+ function staticDefault(param) {
1330
+ const initializer = param.initializer;
1331
+ if (initializer === void 0 || !initializer.evaluated || initializer.ref !== void 0) return;
1332
+ const value = initializer.value;
1333
+ if (value === void 0 || hasRef(value)) return;
1334
+ return { value };
1335
+ }
1336
+ function hasRef(value) {
1337
+ if (isValueRef(value)) return true;
1338
+ if (Array.isArray(value)) return value.some(hasRef);
1339
+ if (typeof value === "object" && value !== null) return Object.values(value).some(hasRef);
1340
+ return false;
1341
+ }
1342
+ /** `@UploadedFile(field?, options?)` / `@UploadedFiles(field?, options?)`: field, options and type. */
1343
+ function uploadOf(ctx, decorator, param, declared, where, multiple) {
1344
+ const name = `@${decorator.name}`;
1345
+ const [first, second] = decorator.args;
1346
+ let field;
1347
+ let options;
1348
+ if (first !== void 0) {
1349
+ if (!first.evaluated) return;
1350
+ if (typeof first.value === "string") {
1351
+ if (first.value === "") {
1352
+ error(ctx, first.location, `${name} expects a non-empty field name`, [`${name}('avatar')`]);
1353
+ return;
1354
+ }
1355
+ field = first.value;
1356
+ options = second;
1357
+ } else if (first.value === void 0) options = second;
1358
+ else if (isPlainObject(first.value)) {
1359
+ if (second !== void 0) {
1360
+ error(ctx, second.location, `${name}(options) takes a single argument`, [`Put the field first: ${name}('field', { ... })`]);
1361
+ return;
1362
+ }
1363
+ options = first;
1364
+ } else {
1365
+ error(ctx, first.location, `${name} expects a field name (string) or options, got ${describe(first.value)}`, [`${name}('avatar'), ${name}('avatar', { maxSize: '5mb' }) or ${name}({ maxSize: '5mb' })`]);
1366
+ return;
1367
+ }
1368
+ }
1369
+ const parsed = uploadOptions(ctx, name, options, multiple);
1370
+ if (parsed === void 0) return;
1371
+ const type = uploadType(ctx, declared.type, multiple);
1372
+ if (!type.ok) {
1373
+ error(ctx, param.typeLocation, `${name} parameter '${param.name}' of ${where} must be typed ${multiple ? "File[]" : "File"}, got ${type.got}`, [multiple ? `${param.name}: File[]` : `${param.name}: File (or ${param.name}?: File when optional)`]);
1374
+ return;
1375
+ }
1376
+ const required = parsed.required ?? !declared.optional;
1377
+ let cast = type.cast;
1378
+ if (!multiple && !required) {
1379
+ const acceptsMissing = declared.type === void 0 || param.optional || param.hasDefault || acceptsUndefined(declared.type) || isRaw(ctx.types.stripNullish(declared.type).type);
1380
+ if (!acceptsMissing && !declared.optional) {
1381
+ error(ctx, options?.location ?? decorator.location, `${name}: '${param.name}' of ${where} is optional ({ required: false }) but typed ${ctx.types.format(declared.type)}`, [`Type it ${param.name}?: File (undefined when no file is sent)`]);
1382
+ return;
1383
+ }
1384
+ cast ||= !acceptsMissing;
1385
+ }
1386
+ return {
1387
+ field,
1388
+ multiple,
1389
+ required,
1390
+ maxSize: parsed.maxSize,
1391
+ maxFiles: parsed.maxFiles,
1392
+ mimeTypes: parsed.mimeTypes,
1393
+ cast
1394
+ };
1395
+ }
1396
+ /** The options object of an upload decorator (`undefined` after an error). */
1397
+ function uploadOptions(ctx, name, arg, multiple) {
1398
+ const result = {
1399
+ required: void 0,
1400
+ maxSize: void 0,
1401
+ maxFiles: void 0,
1402
+ mimeTypes: void 0
1403
+ };
1404
+ if (arg === void 0) return result;
1405
+ if (!arg.evaluated) return;
1406
+ if (arg.value === void 0) return result;
1407
+ const shape = `{ ${(multiple ? UPLOAD_OPTIONS : UPLOAD_OPTIONS.filter((option) => option !== "maxFiles")).map((option) => `${option}?`).join(", ")} }`;
1408
+ if (!isPlainObject(arg.value)) {
1409
+ error(ctx, arg.location, `${name} options must be an object ${shape}, got ${describe(arg.value)}`);
1410
+ return;
1411
+ }
1412
+ let ok = true;
1413
+ const invalid = (message, hints = []) => {
1414
+ ok = false;
1415
+ error(ctx, arg.location, `${name}: ${message}`, hints);
1416
+ };
1417
+ for (const [key, value] of Object.entries(arg.value)) {
1418
+ if (value === void 0) continue;
1419
+ switch (key) {
1420
+ case "required":
1421
+ if (typeof value === "boolean") result.required = value;
1422
+ else invalid(`"required" must be a boolean, got ${describe(value)}`);
1423
+ break;
1424
+ case "maxSize": {
1425
+ const size = parseByteSize(value);
1426
+ if (size === void 0) invalid(`"maxSize" must be a size in bytes, got ${describe(value)}`, ["A non-negative integer (5_242_880), a size with a unit ('5mb', '512kb'; kb = 1024 bytes) or Infinity"]);
1427
+ else result.maxSize = size;
1428
+ break;
1429
+ }
1430
+ case "maxFiles":
1431
+ if (!multiple) invalid("\"maxFiles\" only applies to @UploadedFiles (a single file is expected here)", ["Use @UploadedFiles() to receive several files"]);
1432
+ else if (value === Number.POSITIVE_INFINITY || Number.isSafeInteger(value) && value >= 1) result.maxFiles = value;
1433
+ else invalid(`"maxFiles" must be a positive integer, got ${describe(value)}`);
1434
+ break;
1435
+ case "mimeTypes": {
1436
+ const patterns = Array.isArray(value) ? value : void 0;
1437
+ const wrong = patterns?.filter((pattern) => typeof pattern !== "string" || !isMediaTypePattern(pattern)) ?? [];
1438
+ if (patterns === void 0 || patterns.length === 0) invalid(`"mimeTypes" must be a non-empty array of media types, got ${describe(value)}`, ["mimeTypes: ['image/png', 'image/*']"]);
1439
+ else if (wrong.length > 0) invalid(`invalid media type ${wrong.map(describe).join(", ")} in "mimeTypes"`, ["Use type/subtype, type/* or */* without parameters: 'image/png', 'image/*'"]);
1440
+ else result.mimeTypes = patterns;
1441
+ break;
1442
+ }
1443
+ default: invalid(`unknown option "${key}"`, [`Options: ${shape}`]);
1444
+ }
1445
+ }
1446
+ return ok ? result : void 0;
1447
+ }
1448
+ /** Whether a parameter type can receive uploaded files (and whether the value must be cast). */
1449
+ function uploadType(ctx, type, multiple) {
1450
+ if (type === void 0) return {
1451
+ ok: true,
1452
+ cast: false
1453
+ };
1454
+ const { type: base } = ctx.types.stripNullish(type);
1455
+ if (isRaw(base)) return {
1456
+ ok: true,
1457
+ cast: false
1458
+ };
1459
+ let target = base;
1460
+ if (multiple) target = base.kind === "array" ? base.element : void 0;
1461
+ if (target !== void 0 && isRaw(target)) return {
1462
+ ok: true,
1463
+ cast: false
1464
+ };
1465
+ const cast = target?.kind === "reference" ? FILE_TYPES.get(target.identity) : void 0;
1466
+ return cast === void 0 ? {
1467
+ ok: false,
1468
+ got: ctx.types.format(type)
1469
+ } : {
1470
+ ok: true,
1471
+ cast
1472
+ };
1473
+ }
1474
+ /** Name and/or schema of `@Param(name?, schema?)` / `@Query(name?, schema?)`. */
1475
+ function nameAndSchema(ctx, decorator) {
1476
+ const [first, second] = decorator.args;
1477
+ if (first === void 0) return {
1478
+ ok: true,
1479
+ key: void 0,
1480
+ schema: void 0
1481
+ };
1482
+ if (first.error !== void 0) return { ok: false };
1483
+ if (first.ref !== void 0) {
1484
+ if (second !== void 0) {
1485
+ error(ctx, second.location, `@${decorator.name}(schema) takes a single argument`, [`Put the name first: @${decorator.name}('name', Schema)`]);
1486
+ return { ok: false };
1487
+ }
1488
+ return {
1489
+ ok: true,
1490
+ key: void 0,
1491
+ schema: first.ref
1492
+ };
1493
+ }
1494
+ if (typeof first.value !== "string" || first.value === "") {
1495
+ error(ctx, first.location, `@${decorator.name} expects a name (non-empty string) or a schema, got ${describe(first.value)}`, [`@${decorator.name}('id'), @${decorator.name}('id', Schema) or @${decorator.name}(Schema)`]);
1496
+ return { ok: false };
1497
+ }
1498
+ if (second === void 0) return {
1499
+ ok: true,
1500
+ key: first.value,
1501
+ schema: void 0
1502
+ };
1503
+ const schema = schemaRef(ctx, second, `@${decorator.name}`);
1504
+ return schema ? {
1505
+ ok: true,
1506
+ key: first.value,
1507
+ schema
1508
+ } : { ok: false };
1509
+ }
1510
+ /** A schema argument: a reference to an exported binding. */
1511
+ function schemaRef(ctx, arg, what) {
1512
+ if (arg.error !== void 0) return;
1513
+ if (arg.ref === void 0) {
1514
+ error(ctx, arg.location, `${what} expects a Standard Schema, got ${describe(arg.value)}`, ["Pass a schema declared with `export const Schema = z.object({ ... })` (Zod, Valibot, ArkType...)"]);
1515
+ return;
1516
+ }
1517
+ const symbol = arg.ref.symbol;
1518
+ if (symbol.kind === "declaration" && symbol.declaration.kind === "class" && !hasStandardSchema(ctx, arg.ref)) ctx.report({
1519
+ code: Codes.InvalidDecorator,
1520
+ severity: "warning",
1521
+ message: `${what}(${arg.ref.text}): ${arg.ref.name} is a class, not a Standard Schema; the validation fails at runtime`,
1522
+ location: arg.location,
1523
+ hints: ["Pass a Standard Schema object (Zod, Valibot, ArkType...): classes are not validators"]
1524
+ });
1525
+ return arg.ref;
1526
+ }
1527
+ function hasStandardSchema(ctx, ref) {
1528
+ const info = ref.identity === void 0 ? void 0 : ctx.classInfo(ref.identity);
1529
+ return info === void 0 || info.superClass !== void 0 || info.properties.some((property) => property.static && property.name === "~standard") || info.methods.some((member) => member.static && member.name === "~standard");
1530
+ }
1531
+ /** Coercion of a named parameter / query value / header without schema. */
1532
+ function coercionOf(ctx, declared, param, decorator, where, source, key) {
1533
+ const result = planCoercion(ctx, declared, param, source === "param");
1534
+ const label = `${source === "headers" ? "header" : source} "${key}"`;
1535
+ if (result.ok) {
1536
+ if (source === "query" && declared.type !== void 0 && isAny(declared.type)) unvalidated(ctx, decorator, `${label} of ${where} is typed any`);
1537
+ return result.coercion;
1538
+ }
1539
+ if (source === "query") {
1540
+ unvalidated(ctx, decorator, `${label} of ${where}: ${result.reason}`, result.hints);
1541
+ return {
1542
+ plan: { kind: "raw" },
1543
+ cast: false
1544
+ };
1545
+ }
1546
+ ctx.report({
1547
+ code: Codes.InvalidRoute,
1548
+ severity: "error",
1549
+ message: `Invalid ${label} of ${where}: ${result.reason}`,
1550
+ location: param.typeLocation,
1551
+ hints: [...result.hints]
1552
+ });
1553
+ }
1554
+ /** ZNIGHT017 for a non-primitive `@Body()` / `@Query()` without schema. */
1555
+ function checkUnvalidated(ctx, type, decorator, param, where, what) {
1556
+ if (type !== void 0 && acceptedWithoutSchema(ctx, type)) return;
1557
+ const described = type === void 0 ? "untyped" : `typed ${ctx.types.format(type)}`;
1558
+ unvalidated(ctx, decorator, `the ${what} of ${where} ('${param.name}', ${described}) has no schema`);
1559
+ }
1560
+ function unvalidated(ctx, decorator, detail, extraHints = []) {
1561
+ ctx.report({
1562
+ code: Codes.UnvalidatedInput,
1563
+ severity: ctx.options.strictValidation ? "error" : "warning",
1564
+ message: `Unvalidated input: ${detail}`,
1565
+ location: decorator.location,
1566
+ hints: [
1567
+ ...extraHints.filter((hint) => !hint.startsWith("Or validate")),
1568
+ `Validate it with a Standard Schema: ${schemaHint(decorator.name)}`,
1569
+ "Or type the parameter as unknown and check it yourself"
1570
+ ]
1571
+ });
1572
+ }
1573
+ function schemaHint(decorator) {
1574
+ if (decorator === "Body") return "@Body(Schema) or @Validate(Schema)";
1575
+ if (decorator === "Cookie") return "@Cookie(Schema) for every cookie, or @Cookie('name', Schema)";
1576
+ return `@${decorator}('name', Schema)`;
1577
+ }
1578
+ function acceptedWithoutSchema(ctx, type) {
1579
+ const { type: base } = ctx.types.stripNullish(type);
1580
+ switch (base.kind) {
1581
+ case "primitive": return base.name !== "any" && base.name !== "object" && base.name !== "never" && base.name !== "symbol";
1582
+ case "literal": return true;
1583
+ case "reference":
1584
+ if (UNVALIDATED_REFERENCES.has(base.identity)) return true;
1585
+ return base.identity === "globalThis#Record" && base.typeArgs.length === 2 && isStringLike(base.typeArgs[1]);
1586
+ default: return false;
1587
+ }
1588
+ }
1589
+ function isStringLike(type) {
1590
+ switch (type.kind) {
1591
+ case "primitive": return type.name === "string" || type.name === "undefined" || type.name === "unknown";
1592
+ case "array": return type.element.kind === "primitive" && type.element.name === "string";
1593
+ case "union": return type.types.every(isStringLike);
1594
+ default: return false;
1595
+ }
1596
+ }
1597
+ function isAny(type) {
1598
+ return isRaw(type) && type.kind === "primitive" && type.name === "any";
1599
+ }
1600
+ /**
1601
+ * Decorators of a parameter without duplicates (a parameter with a default value may list its
1602
+ * decorators twice).
1603
+ */
1604
+ function unique(decorators) {
1605
+ const seen = /* @__PURE__ */ new Set();
1606
+ return decorators.filter((decorator) => {
1607
+ if (seen.has(decorator.location.start)) return false;
1608
+ seen.add(decorator.location.start);
1609
+ return true;
1610
+ });
1611
+ }
1612
+ /** `false` when the decorator is applied to a target it does not support (reported by the core). */
1613
+ function validTarget(decorator) {
1614
+ const identity = decorator.identity ?? "";
1615
+ if (ROUTE_METHODS.has(identity)) return decorator.target === "method";
1616
+ if (PARAM_SOURCES.has(identity)) return decorator.target === "parameter";
1617
+ switch (identity) {
1618
+ case Ids.Controller: return decorator.target === "class";
1619
+ case Ids.UseGuard:
1620
+ case Ids.UseInterceptor:
1621
+ case Ids.BodyLimit: return decorator.target === "class" || decorator.target === "method";
1622
+ default: return decorator.target === "method";
1623
+ }
1624
+ }
1625
+ function isPlainObject(value) {
1626
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !isValueRef(value);
1627
+ }
1628
+ function describe(value) {
1629
+ if (value === null) return "null";
1630
+ if (Array.isArray(value)) return "an array";
1631
+ if (isValueRef(value)) return `a reference to '${value.text}'`;
1632
+ if (typeof value === "object") return "an object";
1633
+ if (typeof value === "string") return JSON.stringify(value);
1634
+ return typeof value === "bigint" ? `${value}n` : String(value);
1635
+ }
1636
+ function error(ctx, location, message, hints) {
1637
+ ctx.report({
1638
+ code: Codes.InvalidDecorator,
1639
+ severity: "error",
1640
+ message,
1641
+ location,
1642
+ ...hints ? { hints } : {}
1643
+ });
1644
+ }
1645
+ // src/compiler/generate.ts
1646
+ /** `uploads` helper by kind and presence (`@UploadedFile` / `@UploadedFiles`, required or not). */
1647
+ const UPLOAD_HELPERS = {
1648
+ file: {
1649
+ required: "file",
1650
+ optional: "optionalFile"
1651
+ },
1652
+ files: {
1653
+ required: "files",
1654
+ optional: "optionalFiles"
1655
+ }
1656
+ };
1657
+ /** Generates `metadata.extensions.http` (`undefined` when the application has no controller). */
1658
+ function generateHttp(ctx) {
1659
+ if (ctx.stored.length === 0) return;
1660
+ const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token));
1661
+ const helpers = createHelpers(ctx);
1662
+ const entries = [["controllers", ctx.arr(controllers.map((controller) => controllerNode(ctx, helpers, controller)))]];
1663
+ const groups = groupsNode(ctx, controllers);
1664
+ if (groups !== void 0) entries.push(["groups", groups]);
1665
+ return ctx.obj(entries);
1666
+ }
1667
+ /**
1668
+ * `groups`: the route decorator groups the runtime needs (required, holding several keys, or named
1669
+ * differently from their key), sorted, with sorted keys; `undefined` when there is none, so that
1670
+ * applications without groups keep a byte-identical output.
1671
+ */
1672
+ function groupsNode(ctx, controllers) {
1673
+ const groups = /* @__PURE__ */ new Map();
1674
+ const visit = (entries) => {
1675
+ for (const entry of entries) {
1676
+ const group = groups.get(entry.group);
1677
+ if (group === void 0) groups.set(entry.group, {
1678
+ keys: /* @__PURE__ */ new Set([entry.key]),
1679
+ required: entry.required
1680
+ });
1681
+ else {
1682
+ group.keys.add(entry.key);
1683
+ group.required ||= entry.required;
1684
+ }
1685
+ }
1686
+ };
1687
+ for (const controller of controllers) {
1688
+ visit(controller.meta);
1689
+ for (const route of controller.routes) visit(route.meta);
1690
+ }
1691
+ const needed = [...groups].filter(([name, group]) => group.required || group.keys.size > 1 || !group.keys.has(name)).sort(([a], [b]) => compareStrings(a, b));
1692
+ if (needed.length === 0) return;
1693
+ return ctx.obj(needed.map(([name, group]) => [name, ctx.obj([["keys", ctx.lit([...group.keys].sort(compareStrings))], ["required", group.required]])]));
1694
+ }
1695
+ function createHelpers(ctx) {
1696
+ let coerce;
1697
+ let validate;
1698
+ let uploads;
1699
+ let context;
1700
+ return {
1701
+ coerce: () => {
1702
+ coerce ??= ctx.importFrom(HTTP_PACKAGE, "coerce");
1703
+ return coerce;
1704
+ },
1705
+ validate: () => {
1706
+ validate ??= ctx.importFrom(HTTP_PACKAGE, "validate");
1707
+ return validate;
1708
+ },
1709
+ uploads: () => {
1710
+ uploads ??= ctx.importFrom(HTTP_PACKAGE, "uploads");
1711
+ return uploads;
1712
+ },
1713
+ context: () => {
1714
+ context ??= ctx.importFrom(HTTP_PACKAGE, "HttpContext", { type: true });
1715
+ return context;
1716
+ }
1717
+ };
1718
+ }
1719
+ function controllerNode(ctx, helpers, controller) {
1720
+ return ctx.obj([
1721
+ ["token", controller.token],
1722
+ ["basePath", controller.basePath],
1723
+ ["guards", tokens(ctx, controller.guards)],
1724
+ ["interceptors", tokens(ctx, controller.interceptors)],
1725
+ ["meta", metaNode(ctx, controller.meta)],
1726
+ ["routes", ctx.arr(controller.routes.map((route) => routeNode(ctx, helpers, controller, route)))]
1727
+ ]);
1728
+ }
1729
+ function routeNode(ctx, helpers, controller, route) {
1730
+ const entries = [
1731
+ ["method", route.method],
1732
+ ["path", route.path],
1733
+ ["fullPath", route.fullPath],
1734
+ ["handler", route.handler]
1735
+ ];
1736
+ if (route.status !== void 0) entries.push(["status", route.status]);
1737
+ if (route.headers.length > 0) entries.push(["headers", ctx.obj(route.headers.map(([name, value]) => [name, value]))]);
1738
+ entries.push(["meta", metaNode(ctx, route.meta)], ["guards", tokens(ctx, route.guards)], ["interceptors", tokens(ctx, route.interceptors)], ["params", ctx.arr(route.params.map((param) => paramNode(ctx, param)))]);
1739
+ if (route.bodyLimit !== void 0) entries.push(["bodyLimit", route.bodyLimit]);
1740
+ const uploads = route.params.flatMap((param) => param.upload === void 0 ? [] : [param.upload]);
1741
+ if (uploads.length > 0) entries.push(["uploads", ctx.arr(uploads.map((upload) => uploadNode(ctx, upload)))]);
1742
+ if (route.rawResponse) entries.push(["rawResponse", true]);
1743
+ if (route.sse !== void 0) entries.push(["sse", sseNode(ctx, route.sse)]);
1744
+ entries.push(["invoke", invokeNode(ctx, helpers, controller, route)]);
1745
+ return ctx.obj(entries);
1746
+ }
1747
+ /** `{ heartbeat?, retry?, maxDuration? }` (`{}` without options). */
1748
+ function sseNode(ctx, sse) {
1749
+ const entries = [];
1750
+ if (sse.heartbeat !== void 0) entries.push(["heartbeat", sse.heartbeat]);
1751
+ if (sse.retry !== void 0) entries.push(["retry", sse.retry]);
1752
+ if (sse.maxDuration !== void 0) entries.push(["maxDuration", sse.maxDuration]);
1753
+ return ctx.obj(entries);
1754
+ }
1755
+ function paramNode(ctx, param) {
1756
+ const entries = [["index", param.index], ["source", param.source]];
1757
+ if (param.key !== void 0) entries.push(["name", param.key]);
1758
+ if (param.type !== void 0) entries.push(["type", param.type]);
1759
+ if (param.optional) entries.push(["optional", true]);
1760
+ if (param.schema !== void 0) entries.push(["schema", true]);
1761
+ return ctx.obj(entries);
1762
+ }
1763
+ /** `{ field?, multiple?, required?, maxSize?, maxFiles?, mimeTypes? }` (absent values omitted). */
1764
+ function uploadNode(ctx, upload) {
1765
+ const entries = [];
1766
+ if (upload.field !== void 0) entries.push(["field", upload.field]);
1767
+ if (upload.multiple) entries.push(["multiple", true]);
1768
+ if (upload.required) entries.push(["required", true]);
1769
+ if (upload.maxSize !== void 0) entries.push(["maxSize", upload.maxSize]);
1770
+ if (upload.maxFiles !== void 0) entries.push(["maxFiles", upload.maxFiles]);
1771
+ if (upload.mimeTypes !== void 0) entries.push(["mimeTypes", ctx.lit(upload.mimeTypes)]);
1772
+ return ctx.obj(entries);
1773
+ }
1774
+ function metaNode(ctx, meta) {
1775
+ return ctx.obj(meta.map((entry) => [entry.key, ctx.lit(entry.value)]));
1776
+ }
1777
+ function tokens(ctx, refs) {
1778
+ return ctx.arr(refs.map((ref) => ctx.tokenOf(ref) ?? ref.token));
1779
+ }
1780
+ /**
1781
+ * `(c: Controller, ctx: HttpContext) => c.handler(...args)`: `async` (awaiting every `validate()`)
1782
+ * when the route validates something with a schema, synchronous otherwise.
1783
+ */
1784
+ function invokeNode(ctx, helpers, controller, route) {
1785
+ const async = route.bodySchema !== void 0 || route.params.some((param) => param.schema !== void 0);
1786
+ const params = [["c", ctx.importType(controller.identity)]];
1787
+ if (route.params.length > 0 || route.bodySchema !== void 0) params.push(["ctx", helpers.context()]);
1788
+ return ctx.arrow(params, (c, http) => {
1789
+ const args = [];
1790
+ for (const param of route.params) args[param.index] = argumentNode(ctx, helpers, http, param);
1791
+ const call = ctx.call(ctx.member(c, route.handler), Array.from(args, (arg) => arg ?? ctx.lit(void 0)));
1792
+ if (route.bodySchema === void 0) return call;
1793
+ const body = ctx.code`await ${ctx.call(ctx.member(http, "parseBody"))}`;
1794
+ const check = validation(ctx, helpers, route.bodySchema, body, "body");
1795
+ return ctx.code`{ ${check}; return ${call} }`;
1796
+ }, { async });
1797
+ }
1798
+ /** The expression passed to the handler for one parameter. */
1799
+ function argumentNode(ctx, helpers, http, param) {
1800
+ switch (param.source) {
1801
+ case "context": return http;
1802
+ case "request": return ctx.code`${ctx.member(http, "request")} as never`;
1803
+ case "response": return ctx.code`${ctx.member(http, "response")} as never`;
1804
+ case "body": {
1805
+ const body = ctx.member(http, "body");
1806
+ return param.schema === void 0 ? ctx.code`${body} as never` : validation(ctx, helpers, param.schema, body, "body");
1807
+ }
1808
+ case "file":
1809
+ case "files": {
1810
+ const upload = param.upload;
1811
+ const helper = UPLOAD_HELPERS[upload.multiple ? "files" : "file"][upload.required ? "required" : "optional"];
1812
+ const call = ctx.call(ctx.member(helpers.uploads(), helper), [http, ctx.lit(upload.field)]);
1813
+ return upload.cast ? ctx.code`${call} as never` : call;
1814
+ }
1815
+ case "headers":
1816
+ case "param":
1817
+ case "query": {
1818
+ const record = ctx.member(http, param.source === "param" ? "params" : param.source);
1819
+ if (param.key === void 0) {
1820
+ const location = param.source === "param" ? "params" : param.source;
1821
+ return param.schema === void 0 ? ctx.code`${record} as never` : validation(ctx, helpers, param.schema, record, location);
1822
+ }
1823
+ const value = ctx.code`${record}[${param.key}]`;
1824
+ const label = `${param.source === "headers" ? "header" : param.source} "${param.key}"`;
1825
+ if (param.schema !== void 0) return validation(ctx, helpers, param.schema, value, label);
1826
+ return coercion(ctx, helpers, param.coercion ?? {
1827
+ plan: { kind: "raw" },
1828
+ cast: false
1829
+ }, value, label);
1830
+ }
1831
+ case "cookie":
1832
+ case "signedCookie": {
1833
+ if (param.key === void 0) {
1834
+ const cookies = ctx.member(http, "cookies");
1835
+ return param.schema === void 0 ? ctx.code`${cookies} as never` : validation(ctx, helpers, param.schema, cookies, "cookies");
1836
+ }
1837
+ const value = param.source === "cookie" ? ctx.code`${ctx.member(http, "cookies")}[${param.key}]` : ctx.call(ctx.member(http, "signedCookie"), [param.key]);
1838
+ const label = `cookie "${param.key}"`;
1839
+ if (param.schema !== void 0) return validation(ctx, helpers, param.schema, value, label);
1840
+ return coercion(ctx, helpers, param.coercion ?? {
1841
+ plan: { kind: "raw" },
1842
+ cast: false
1843
+ }, value, label);
1844
+ }
1845
+ }
1846
+ }
1847
+ /** `await validate(schema, value, location)`. */
1848
+ function validation(ctx, helpers, schema, value, location) {
1849
+ return ctx.code`await ${ctx.call(helpers.validate(), [
1850
+ ctx.ref(schema),
1851
+ value,
1852
+ location
1853
+ ])}`;
1854
+ }
1855
+ /** `coerce.X(value, label, ...)`, cast with `as never` when the result does not have the declared type. */
1856
+ function coercion(ctx, helpers, coercion, value, label) {
1857
+ const node = coerceCall(ctx, helpers, coercion.plan, value, label);
1858
+ return coercion.cast ? ctx.code`${node} as never` : node;
1859
+ }
1860
+ function coerceCall(ctx, helpers, plan, value, label) {
1861
+ const helper = (name) => ctx.member(helpers.coerce(), name);
1862
+ switch (plan.kind) {
1863
+ case "raw": return ctx.code`${value} as never`;
1864
+ case "enum": return ctx.call(helper("enum"), [
1865
+ value,
1866
+ label,
1867
+ ctx.lit(plan.values)
1868
+ ]);
1869
+ case "array":
1870
+ case "optional": return ctx.call(helper(plan.kind), [
1871
+ value,
1872
+ label,
1873
+ itemCoercer(ctx, helpers, plan.item)
1874
+ ]);
1875
+ default: return ctx.call(helper(plan.kind), [value, label]);
1876
+ }
1877
+ }
1878
+ /** A `Coercer` value: `coerce.number`, or `(v, l) => coerce.enum(v, l, [...])`. */
1879
+ function itemCoercer(ctx, helpers, plan) {
1880
+ switch (plan.kind) {
1881
+ case "enum":
1882
+ case "array":
1883
+ case "optional": return ctx.arrow(["v", "l"], (v, l) => coerceCall(ctx, helpers, plan, v, l));
1884
+ case "raw": return ctx.arrow(["v"], (v) => v);
1885
+ default: return ctx.member(helpers.coerce(), plan.kind);
1886
+ }
1887
+ }
1888
+ // src/compiler/validate.ts
1889
+ /** Validates the routes of every controller. */
1890
+ function validateHttp(ctx) {
1891
+ const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token));
1892
+ const registered = /* @__PURE__ */ new Map();
1893
+ for (const controller of controllers) {
1894
+ const baseIssue = checkRoutePath(controller.rawBasePath);
1895
+ if (baseIssue !== void 0) {
1896
+ invalidRoute(ctx, controller.pathLocation, `Invalid base path ${JSON.stringify(controller.rawBasePath)} of ${controller.displayName}: ${baseIssue.message}`, GRAMMAR_HINTS);
1897
+ continue;
1898
+ }
1899
+ for (const route of controller.routes) {
1900
+ const where = `${controller.displayName}.${route.handler}`;
1901
+ const ownIssue = checkRoutePath(route.rawPath);
1902
+ const issue = ownIssue ?? checkRoutePath(route.fullPath);
1903
+ if (issue !== void 0) {
1904
+ const path = ownIssue === void 0 ? route.fullPath : route.rawPath;
1905
+ invalidRoute(ctx, route.pathLocation, `Invalid route path ${JSON.stringify(path)} of ${where}: ${issue.message}`, GRAMMAR_HINTS);
1906
+ continue;
1907
+ }
1908
+ const parsed = parseRoutePath(route.fullPath);
1909
+ checkParams(ctx, route, parsed, where);
1910
+ const key = `${route.method} ${parsed.shape}`;
1911
+ const previous = registered.get(key);
1912
+ if (previous === void 0) {
1913
+ registered.set(key, {
1914
+ where,
1915
+ route
1916
+ });
1917
+ continue;
1918
+ }
1919
+ ctx.report({
1920
+ code: Codes.InvalidRoute,
1921
+ severity: "error",
1922
+ message: `Duplicate route ${route.method} ${route.fullPath}: ${where} conflicts with ${previous.where} (${previous.route.method} ${previous.route.fullPath})`,
1923
+ location: route.location,
1924
+ hints: ["Two routes with the same method and the same path shape (parameter names aside) cannot coexist"],
1925
+ related: [{
1926
+ message: `${previous.where} is declared here`,
1927
+ location: previous.route.location
1928
+ }]
1929
+ });
1930
+ }
1931
+ }
1932
+ validateDecisions(ctx, controllers);
1933
+ }
1934
+ /**
1935
+ * Every route decorator definition, sorted by identity: the applied ones (first usage) and, when
1936
+ * the compiler provides `definitionsOf`, the ones applied nowhere (invalid ones are left to the
1937
+ * usages, which report them).
1938
+ */
1939
+ function routeDecoratorDefinitions(ctx, controllers) {
1940
+ const definitions = /* @__PURE__ */ new Map();
1941
+ const visit = (entries) => {
1942
+ for (const entry of entries) {
1943
+ const known = definitions.get(entry.identity);
1944
+ if (known === void 0) definitions.set(entry.identity, {
1945
+ identity: entry.identity,
1946
+ name: entry.decorator,
1947
+ key: entry.key,
1948
+ group: entry.group,
1949
+ required: entry.required,
1950
+ definition: entry.definition,
1951
+ flag: entry.value === true
1952
+ });
1953
+ else if (entry.value !== true) known.flag = false;
1954
+ }
1955
+ };
1956
+ for (const controller of controllers) {
1957
+ visit(controller.meta);
1958
+ for (const route of controller.routes) visit(route.meta);
1959
+ }
1960
+ const all = typeof ctx.definitionsOf === "function" ? ctx.definitionsOf(Ids.createRouteDecorator) : [];
1961
+ for (const found of all) {
1962
+ const [keyArg, optionsArg, ...extra] = found.args;
1963
+ const key = keyArg?.evaluated === true ? keyArg.value : void 0;
1964
+ if (typeof key !== "string" || key === "" || extra.length > 0 || optionsArg !== void 0 && !optionsArg.evaluated) continue;
1965
+ const parsed = parseRouteDecoratorOptions(key, optionsArg?.value);
1966
+ if ("error" in parsed) continue;
1967
+ const flag = found.typeArgs.length === 0;
1968
+ const known = definitions.get(found.identity);
1969
+ if (known !== void 0) {
1970
+ known.flag &&= flag;
1971
+ continue;
1972
+ }
1973
+ definitions.set(found.identity, {
1974
+ identity: found.identity,
1975
+ name: found.name,
1976
+ key,
1977
+ ...parsed,
1978
+ definition: found.location,
1979
+ flag
1980
+ });
1981
+ }
1982
+ return [...definitions.values()].sort((a, b) => compareStrings(a.identity, b.identity));
1983
+ }
1984
+ /**
1985
+ * Route decorator groups (RFC 0001 §5.1): every definition of a key uses the same options and the
1986
+ * decorators of a group agree on `required` (ZNIGHT005, at the second definition); every route has
1987
+ * a decision of every required group, its own or its controller's (ZNIGHT023).
1988
+ */
1989
+ function validateDecisions(ctx, controllers) {
1990
+ const definitions = routeDecoratorDefinitions(ctx, controllers);
1991
+ if (definitions.length === 0) return;
1992
+ const byKey = /* @__PURE__ */ new Map();
1993
+ const byGroup = /* @__PURE__ */ new Map();
1994
+ const members = /* @__PURE__ */ new Map();
1995
+ for (const definition of definitions) {
1996
+ const sameKey = byKey.get(definition.key);
1997
+ if (sameKey !== void 0 && (sameKey.group !== definition.group || sameKey.required !== definition.required)) {
1998
+ ctx.report({
1999
+ code: Codes.InvalidDecorator,
2000
+ severity: "error",
2001
+ message: `The route decorator ${definition.name} defines the key "${definition.key}" with ${describeOptions(definition)}, but ${sameKey.name} defines it with ${describeOptions(sameKey)}`,
2002
+ location: definition.definition,
2003
+ hints: ["Every definition of a key must use the same options: share one definition"],
2004
+ related: [{
2005
+ message: `${sameKey.name} is defined here`,
2006
+ location: sameKey.definition
2007
+ }]
2008
+ });
2009
+ continue;
2010
+ }
2011
+ const sameGroup = byGroup.get(definition.group);
2012
+ if (sameGroup !== void 0 && sameGroup.required !== definition.required) {
2013
+ ctx.report({
2014
+ code: Codes.InvalidDecorator,
2015
+ severity: "error",
2016
+ message: `The "${definition.group}" group is ${definition.required ? "" : "not "}required for ${definition.name} but ${sameGroup.required ? "" : "not "}required for ${sameGroup.name}`,
2017
+ location: definition.definition,
2018
+ hints: [`All the decorators of the "${definition.group}" group must agree on required`],
2019
+ related: [{
2020
+ message: `${sameGroup.name} is defined here`,
2021
+ location: sameGroup.definition
2022
+ }]
2023
+ });
2024
+ continue;
2025
+ }
2026
+ byKey.set(definition.key, sameKey ?? definition);
2027
+ byGroup.set(definition.group, sameGroup ?? definition);
2028
+ const list = members.get(definition.group) ?? [];
2029
+ list.push(definition);
2030
+ members.set(definition.group, list);
2031
+ }
2032
+ const required = [...byGroup.values()].filter((definition) => definition.required);
2033
+ if (required.length === 0) return;
2034
+ for (const controller of controllers) {
2035
+ const controllerGroups = new Set(controller.meta.map((entry) => entry.group));
2036
+ for (const route of controller.routes) {
2037
+ const decided = /* @__PURE__ */ new Set([...controllerGroups, ...route.meta.map((entry) => entry.group)]);
2038
+ for (const group of required) {
2039
+ if (decided.has(group.group)) continue;
2040
+ const inOrder = [...members.get(group.group) ?? [group]].sort((a, b) => compareStrings(a.definition.file, b.definition.file) || a.definition.start - b.definition.start);
2041
+ const decorators = inOrder.map((definition) => `@${definition.name}(${definition.flag ? "" : "..."})`);
2042
+ const listed = decorators.length === 1 ? decorators[0] : `${decorators.slice(0, -1).join(", ")} or ${decorators.at(-1)}`;
2043
+ ctx.report({
2044
+ code: Codes.MissingRouteDecision,
2045
+ severity: "error",
2046
+ message: `Route ${route.method} ${route.fullPath} (${controller.displayName}.${route.handler}) has no "${group.group}" decision`,
2047
+ location: route.location,
2048
+ hints: [`Add ${listed} to the route or to ${controller.displayName}`],
2049
+ related: [{
2050
+ message: `The "${group.group}" group is required`,
2051
+ location: (inOrder[0] ?? group).definition
2052
+ }, {
2053
+ message: `${controller.displayName} is declared here`,
2054
+ location: controller.location
2055
+ }]
2056
+ });
2057
+ }
2058
+ }
2059
+ }
2060
+ }
2061
+ function describeOptions(entry) {
2062
+ const group = entry.group === entry.key ? "no group" : `group "${entry.group}"`;
2063
+ return entry.required ? `${group}, required` : group;
2064
+ }
2065
+ const GRAMMAR_HINTS = ["Paths are made of static segments (letters, digits and - . _ ~ ! $ & ' + , ; = @), :name parameters and a trailing * wildcard"];
2066
+ /** `:param` of the path ↔ `@Param('name')` parameters of the handler. */
2067
+ function checkParams(ctx, route, path, where) {
2068
+ const declared = new Set(path.params);
2069
+ const consumed = /* @__PURE__ */ new Set();
2070
+ let everything = false;
2071
+ for (const param of route.params) {
2072
+ if (param.source === "context" || param.source === "request") {
2073
+ everything = true;
2074
+ continue;
2075
+ }
2076
+ if (param.source !== "param") continue;
2077
+ if (param.key === void 0) {
2078
+ everything = true;
2079
+ continue;
2080
+ }
2081
+ if (declared.has(param.key)) {
2082
+ consumed.add(param.key);
2083
+ continue;
2084
+ }
2085
+ const available = path.params.filter((name) => name !== "*").map((name) => `:${name}`);
2086
+ invalidRoute(ctx, param.location, `@Param('${param.key}') of ${where}: the route ${route.method} ${route.fullPath} has no ${param.key === "*" ? "wildcard" : `":${param.key}" parameter`}`, [available.length > 0 ? `Parameters of the route: ${available.join(", ")}` : `Add ":${param.key}" to the route path`]);
2087
+ }
2088
+ if (everything || !route.complete) return;
2089
+ for (const name of path.params) if (name !== "*" && !consumed.has(name)) invalidRoute(ctx, route.pathLocation, `Route ${route.method} ${route.fullPath} declares ":${name}" but ${where} has no @Param('${name}') parameter`, [`Add a parameter: @Param('${name}') ${name}: string`]);
2090
+ }
2091
+ function invalidRoute(ctx, location, message, hints) {
2092
+ ctx.report({
2093
+ code: Codes.InvalidRoute,
2094
+ severity: "error",
2095
+ message,
2096
+ location,
2097
+ hints
2098
+ });
2099
+ }
2100
+ // src/compiler/index.ts
2101
+ const ROUTE = {
2102
+ targets: ["method"],
2103
+ params: ["static"]
2104
+ };
2105
+ const NAME_OR_SCHEMA = {
2106
+ targets: ["parameter"],
2107
+ params: ["value", "value"]
2108
+ };
2109
+ const NO_ARGUMENT = {
2110
+ targets: ["parameter"],
2111
+ params: []
2112
+ };
2113
+ const CLASS_REFS = {
2114
+ targets: ["class", "method"],
2115
+ params: [],
2116
+ rest: "value"
2117
+ };
2118
+ const UPLOAD = {
2119
+ targets: ["parameter"],
2120
+ params: ["static", "static"]
2121
+ };
2122
+ /** Decorators owned by the extension, by canonical identity (static / value arguments, §4.6). */
2123
+ const decorators = Object.freeze({
2124
+ [Ids.Controller]: {
2125
+ targets: ["class"],
2126
+ params: ["static"]
2127
+ },
2128
+ ...Object.fromEntries([...ROUTE_METHODS.keys()].map((identity) => [identity, ROUTE])),
2129
+ [Ids.Param]: NAME_OR_SCHEMA,
2130
+ [Ids.Query]: NAME_OR_SCHEMA,
2131
+ [Ids.Body]: {
2132
+ targets: ["parameter"],
2133
+ params: ["value"]
2134
+ },
2135
+ [Ids.Headers]: {
2136
+ targets: ["parameter"],
2137
+ params: ["static"]
2138
+ },
2139
+ [Ids.Req]: NO_ARGUMENT,
2140
+ [Ids.Res]: NO_ARGUMENT,
2141
+ [Ids.Ctx]: NO_ARGUMENT,
2142
+ [Ids.HttpCode]: {
2143
+ targets: ["method"],
2144
+ params: ["static"]
2145
+ },
2146
+ [Ids.Header]: {
2147
+ targets: ["method"],
2148
+ params: ["static", "static"]
2149
+ },
2150
+ [Ids.UseGuard]: CLASS_REFS,
2151
+ [Ids.UseInterceptor]: CLASS_REFS,
2152
+ [Ids.Validate]: {
2153
+ targets: ["method"],
2154
+ params: ["value"]
2155
+ },
2156
+ [Ids.UploadedFile]: UPLOAD,
2157
+ [Ids.UploadedFiles]: UPLOAD,
2158
+ [Ids.BodyLimit]: {
2159
+ targets: ["class", "method"],
2160
+ params: ["static"]
2161
+ },
2162
+ [Ids.Cookie]: NAME_OR_SCHEMA,
2163
+ [Ids.SignedCookie]: NAME_OR_SCHEMA,
2164
+ [Ids.Sse]: {
2165
+ targets: ["method"],
2166
+ params: ["static", "static"]
2167
+ }
2168
+ });
2169
+ /** The `@znight/http` compiler extension. */
2170
+ const http = Object.freeze({
2171
+ apiVersion: 1,
2172
+ name: EXTENSION_NAME,
2173
+ decorators,
2174
+ analyzeClass: analyzeHttpClass,
2175
+ validate: validateHttp,
2176
+ generate: generateHttp
2177
+ });
2178
+ export { http as default };
2179
+
2180
+ //# sourceMappingURL=compiler.mjs.map