@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,288 @@
1
+ /**
2
+ * `validate` of the `@znight/http` compiler extension: route paths (grammar of `@znight/http/path`),
3
+ * `:param` ↔ `@Param` consistency and duplicate routes across every controller (ZNIGHT007); route
4
+ * decorator groups: consistent definitions (ZNIGHT005) and a decision of every required group on
5
+ * every route (ZNIGHT023).
6
+ */
7
+ import type { Location, ValidateContext } from '@znight/compiler'
8
+ import type { RoutePath } from '../path.ts'
9
+ import type { HttpControllerData, HttpRouteData, MetaEntry } from './model.ts'
10
+
11
+ import { checkRoutePath, compareStrings, parseRoutePath, WILDCARD_PARAM } from '../path.ts'
12
+ import { Codes, Ids } from './model.ts'
13
+ import { parseRouteDecoratorOptions } from './route-decorators.ts'
14
+
15
+ type Context = ValidateContext<HttpControllerData>
16
+
17
+ interface RegisteredRoute {
18
+ readonly where: string
19
+ readonly route: HttpRouteData
20
+ }
21
+
22
+ /** Validates the routes of every controller. */
23
+ export function validateHttp(ctx: Context): void {
24
+ const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token))
25
+ const registered = new Map<string, RegisteredRoute>()
26
+ for (const controller of controllers) {
27
+ const baseIssue = checkRoutePath(controller.rawBasePath)
28
+ if (baseIssue !== undefined) {
29
+ invalidRoute(
30
+ ctx,
31
+ controller.pathLocation,
32
+ `Invalid base path ${JSON.stringify(controller.rawBasePath)} of ${controller.displayName}: ${baseIssue.message}`,
33
+ GRAMMAR_HINTS,
34
+ )
35
+ continue
36
+ }
37
+ for (const route of controller.routes) {
38
+ const where = `${controller.displayName}.${route.handler}`
39
+ const ownIssue = checkRoutePath(route.rawPath)
40
+ const issue = ownIssue ?? checkRoutePath(route.fullPath)
41
+ if (issue !== undefined) {
42
+ const path = ownIssue === undefined ? route.fullPath : route.rawPath
43
+ invalidRoute(
44
+ ctx,
45
+ route.pathLocation,
46
+ `Invalid route path ${JSON.stringify(path)} of ${where}: ${issue.message}`,
47
+ GRAMMAR_HINTS,
48
+ )
49
+ continue
50
+ }
51
+ const parsed = parseRoutePath(route.fullPath)
52
+ checkParams(ctx, route, parsed, where)
53
+ const key = `${route.method} ${parsed.shape}`
54
+ const previous = registered.get(key)
55
+ if (previous === undefined) {
56
+ registered.set(key, { where, route })
57
+ continue
58
+ }
59
+ ctx.report({
60
+ code: Codes.InvalidRoute,
61
+ severity: 'error',
62
+ message: `Duplicate route ${route.method} ${route.fullPath}: ${where} conflicts with ${previous.where} (${previous.route.method} ${previous.route.fullPath})`,
63
+ location: route.location,
64
+ hints: ['Two routes with the same method and the same path shape (parameter names aside) cannot coexist'],
65
+ related: [{ message: `${previous.where} is declared here`, location: previous.route.location }],
66
+ })
67
+ }
68
+ }
69
+ validateDecisions(ctx, controllers)
70
+ }
71
+
72
+ /** A route decorator definition seen during the compile (applied, or found by `definitionsOf`). */
73
+ interface Definition {
74
+ readonly identity: string
75
+ readonly name: string
76
+ readonly key: string
77
+ readonly group: string
78
+ readonly required: boolean
79
+ readonly definition: Location
80
+ /** A flag (`@Public()`): no type argument, or every usage without argument. */
81
+ flag: boolean
82
+ }
83
+
84
+ /**
85
+ * Every route decorator definition, sorted by identity: the applied ones (first usage) and, when
86
+ * the compiler provides `definitionsOf`, the ones applied nowhere (invalid ones are left to the
87
+ * usages, which report them).
88
+ */
89
+ function routeDecoratorDefinitions(ctx: Context, controllers: readonly HttpControllerData[]): Definition[] {
90
+ const definitions = new Map<string, Definition>()
91
+ const visit = (entries: readonly MetaEntry[]): void => {
92
+ for (const entry of entries) {
93
+ const known = definitions.get(entry.identity)
94
+ if (known === undefined) {
95
+ definitions.set(entry.identity, {
96
+ identity: entry.identity,
97
+ name: entry.decorator,
98
+ key: entry.key,
99
+ group: entry.group,
100
+ required: entry.required,
101
+ definition: entry.definition,
102
+ flag: entry.value === true,
103
+ })
104
+ } else if (entry.value !== true) {
105
+ known.flag = false
106
+ }
107
+ }
108
+ }
109
+ for (const controller of controllers) {
110
+ visit(controller.meta)
111
+ for (const route of controller.routes) visit(route.meta)
112
+ }
113
+ // compilers before RFC 0001 have no definitionsOf: only applied decorators are known then
114
+ const all = typeof ctx.definitionsOf === 'function' ? ctx.definitionsOf(Ids.createRouteDecorator) : []
115
+ for (const found of all) {
116
+ const [keyArg, optionsArg, ...extra] = found.args
117
+ const key = keyArg?.evaluated === true ? keyArg.value : undefined
118
+ if (
119
+ typeof key !== 'string' ||
120
+ key === '' ||
121
+ extra.length > 0 ||
122
+ (optionsArg !== undefined && !optionsArg.evaluated)
123
+ ) {
124
+ continue
125
+ }
126
+ const parsed = parseRouteDecoratorOptions(key, optionsArg?.value)
127
+ if ('error' in parsed) {
128
+ continue
129
+ }
130
+ const flag = found.typeArgs.length === 0
131
+ const known = definitions.get(found.identity)
132
+ if (known !== undefined) {
133
+ known.flag &&= flag
134
+ continue
135
+ }
136
+ definitions.set(found.identity, {
137
+ identity: found.identity,
138
+ name: found.name,
139
+ key,
140
+ ...parsed,
141
+ definition: found.location,
142
+ flag,
143
+ })
144
+ }
145
+ return [...definitions.values()].sort((a, b) => compareStrings(a.identity, b.identity))
146
+ }
147
+
148
+ /**
149
+ * Route decorator groups (RFC 0001 §5.1): every definition of a key uses the same options and the
150
+ * decorators of a group agree on `required` (ZNIGHT005, at the second definition); every route has
151
+ * a decision of every required group, its own or its controller's (ZNIGHT023).
152
+ */
153
+ function validateDecisions(ctx: Context, controllers: readonly HttpControllerData[]): void {
154
+ const definitions = routeDecoratorDefinitions(ctx, controllers)
155
+ if (definitions.length === 0) {
156
+ return
157
+ }
158
+ const byKey = new Map<string, Definition>()
159
+ const byGroup = new Map<string, Definition>()
160
+ const members = new Map<string, Definition[]>()
161
+ for (const definition of definitions) {
162
+ const sameKey = byKey.get(definition.key)
163
+ if (sameKey !== undefined && (sameKey.group !== definition.group || sameKey.required !== definition.required)) {
164
+ ctx.report({
165
+ code: Codes.InvalidDecorator,
166
+ severity: 'error',
167
+ message: `The route decorator ${definition.name} defines the key "${definition.key}" with ${describeOptions(definition)}, but ${sameKey.name} defines it with ${describeOptions(sameKey)}`,
168
+ location: definition.definition,
169
+ hints: ['Every definition of a key must use the same options: share one definition'],
170
+ related: [{ message: `${sameKey.name} is defined here`, location: sameKey.definition }],
171
+ })
172
+ continue
173
+ }
174
+ const sameGroup = byGroup.get(definition.group)
175
+ if (sameGroup !== undefined && sameGroup.required !== definition.required) {
176
+ ctx.report({
177
+ code: Codes.InvalidDecorator,
178
+ severity: 'error',
179
+ message: `The "${definition.group}" group is ${definition.required ? '' : 'not '}required for ${definition.name} but ${sameGroup.required ? '' : 'not '}required for ${sameGroup.name}`,
180
+ location: definition.definition,
181
+ hints: [`All the decorators of the "${definition.group}" group must agree on required`],
182
+ related: [{ message: `${sameGroup.name} is defined here`, location: sameGroup.definition }],
183
+ })
184
+ continue
185
+ }
186
+ byKey.set(definition.key, sameKey ?? definition)
187
+ byGroup.set(definition.group, sameGroup ?? definition)
188
+ const list = members.get(definition.group) ?? []
189
+ list.push(definition)
190
+ members.set(definition.group, list)
191
+ }
192
+ const required = [...byGroup.values()].filter(definition => definition.required)
193
+ if (required.length === 0) {
194
+ return
195
+ }
196
+ for (const controller of controllers) {
197
+ const controllerGroups = new Set(controller.meta.map(entry => entry.group))
198
+ for (const route of controller.routes) {
199
+ const decided = new Set([...controllerGroups, ...route.meta.map(entry => entry.group)])
200
+ for (const group of required) {
201
+ if (decided.has(group.group)) {
202
+ continue
203
+ }
204
+ // the decorators of the group in source order: `@Public(), @Authenticated() or @Roles(...)`
205
+ const inOrder = [...(members.get(group.group) ?? [group])].sort(
206
+ (a, b) => compareStrings(a.definition.file, b.definition.file) || a.definition.start - b.definition.start,
207
+ )
208
+ const decorators = inOrder.map(definition => `@${definition.name}(${definition.flag ? '' : '...'})`)
209
+ const listed =
210
+ decorators.length === 1
211
+ ? (decorators[0] as string)
212
+ : `${decorators.slice(0, -1).join(', ')} or ${decorators.at(-1) as string}`
213
+ ctx.report({
214
+ code: Codes.MissingRouteDecision,
215
+ severity: 'error',
216
+ message: `Route ${route.method} ${route.fullPath} (${controller.displayName}.${route.handler}) has no "${group.group}" decision`,
217
+ location: route.location,
218
+ hints: [`Add ${listed} to the route or to ${controller.displayName}`],
219
+ related: [
220
+ { message: `The "${group.group}" group is required`, location: (inOrder[0] ?? group).definition },
221
+ { message: `${controller.displayName} is declared here`, location: controller.location },
222
+ ],
223
+ })
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ function describeOptions(entry: Definition): string {
230
+ const group = entry.group === entry.key ? 'no group' : `group "${entry.group}"`
231
+ return entry.required ? `${group}, required` : group
232
+ }
233
+
234
+ const GRAMMAR_HINTS: readonly string[] = [
235
+ "Paths are made of static segments (letters, digits and - . _ ~ ! $ & ' + , ; = @), :name parameters and a trailing * wildcard",
236
+ ]
237
+
238
+ /** `:param` of the path ↔ `@Param('name')` parameters of the handler. */
239
+ function checkParams(ctx: Context, route: HttpRouteData, path: RoutePath, where: string): void {
240
+ const declared = new Set(path.params)
241
+ const consumed = new Set<string>()
242
+ let everything = false
243
+ for (const param of route.params) {
244
+ if (param.source === 'context' || param.source === 'request') {
245
+ everything = true // ctx.params / the native request give access to every parameter
246
+ continue
247
+ }
248
+ if (param.source !== 'param') {
249
+ continue
250
+ }
251
+ if (param.key === undefined) {
252
+ everything = true
253
+ continue
254
+ }
255
+ if (declared.has(param.key)) {
256
+ consumed.add(param.key)
257
+ continue
258
+ }
259
+ const available = path.params.filter(name => name !== WILDCARD_PARAM).map(name => `:${name}`)
260
+ invalidRoute(
261
+ ctx,
262
+ param.location,
263
+ `@Param('${param.key}') of ${where}: the route ${route.method} ${route.fullPath} has no ${param.key === WILDCARD_PARAM ? 'wildcard' : `":${param.key}" parameter`}`,
264
+ [
265
+ available.length > 0
266
+ ? `Parameters of the route: ${available.join(', ')}`
267
+ : `Add ":${param.key}" to the route path`,
268
+ ],
269
+ )
270
+ }
271
+ if (everything || !route.complete) {
272
+ return // a parameter could not be analysed (already reported): do not guess what it consumes
273
+ }
274
+ for (const name of path.params) {
275
+ if (name !== WILDCARD_PARAM && !consumed.has(name)) {
276
+ invalidRoute(
277
+ ctx,
278
+ route.pathLocation,
279
+ `Route ${route.method} ${route.fullPath} declares ":${name}" but ${where} has no @Param('${name}') parameter`,
280
+ [`Add a parameter: @Param('${name}') ${name}: string`],
281
+ )
282
+ }
283
+ }
284
+ }
285
+
286
+ function invalidRoute(ctx: Context, location: Location, message: string, hints: readonly string[]): void {
287
+ ctx.report({ code: Codes.InvalidRoute, severity: 'error', message, location, hints })
288
+ }