@znight/http 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2333 -2
  3. package/dist/adapter-CGSceVEN.d.mts +876 -0
  4. package/dist/compiler.d.mts +156 -0
  5. package/dist/compiler.mjs +2180 -0
  6. package/dist/compiler.mjs.map +1 -0
  7. package/dist/express.d.mts +39 -0
  8. package/dist/express.mjs +135 -0
  9. package/dist/express.mjs.map +1 -0
  10. package/dist/fastify.d.mts +33 -0
  11. package/dist/fastify.mjs +191 -0
  12. package/dist/fastify.mjs.map +1 -0
  13. package/dist/hono.d.mts +49 -0
  14. package/dist/hono.mjs +319 -0
  15. package/dist/hono.mjs.map +1 -0
  16. package/dist/index.d.mts +1254 -0
  17. package/dist/index.mjs +5618 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/node-BUnkdSsa.mjs +183 -0
  20. package/dist/node-BUnkdSsa.mjs.map +1 -0
  21. package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
  22. package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
  23. package/dist/node.d.mts +30 -0
  24. package/dist/node.mjs +1 -0
  25. package/dist/path-JI87C4k8.d.mts +109 -0
  26. package/dist/path.d.mts +2 -0
  27. package/dist/path.mjs +245 -0
  28. package/dist/path.mjs.map +1 -0
  29. package/dist/size-D8rsQcV0.mjs +184 -0
  30. package/dist/size-D8rsQcV0.mjs.map +1 -0
  31. package/dist/utils-DCbu3a4w.mjs +27 -0
  32. package/dist/utils-DCbu3a4w.mjs.map +1 -0
  33. package/package.json +142 -4
  34. package/src/adapter.ts +240 -0
  35. package/src/adapters/express.ts +201 -0
  36. package/src/adapters/fastify.ts +280 -0
  37. package/src/adapters/hono.ts +488 -0
  38. package/src/adapters/node-io.ts +368 -0
  39. package/src/adapters/node.ts +135 -0
  40. package/src/adapters/shared.ts +508 -0
  41. package/src/body.ts +135 -0
  42. package/src/coerce.ts +195 -0
  43. package/src/compiler/analyze.ts +1513 -0
  44. package/src/compiler/ast.ts +90 -0
  45. package/src/compiler/coercion.ts +267 -0
  46. package/src/compiler/generate.ts +374 -0
  47. package/src/compiler/index.ts +76 -0
  48. package/src/compiler/model.ts +260 -0
  49. package/src/compiler/route-decorators.ts +405 -0
  50. package/src/compiler/validate.ts +288 -0
  51. package/src/context.ts +533 -0
  52. package/src/cookie-settings.ts +68 -0
  53. package/src/cookie-signer.ts +106 -0
  54. package/src/cookies.ts +726 -0
  55. package/src/cors.ts +349 -0
  56. package/src/decisions.ts +331 -0
  57. package/src/decorators.ts +486 -0
  58. package/src/event-channel.ts +197 -0
  59. package/src/exceptions.ts +257 -0
  60. package/src/files.ts +429 -0
  61. package/src/headers.ts +39 -0
  62. package/src/index.ts +185 -0
  63. package/src/inject-sse.ts +309 -0
  64. package/src/interfaces.ts +101 -0
  65. package/src/metadata.ts +162 -0
  66. package/src/mime.ts +157 -0
  67. package/src/multipart.ts +597 -0
  68. package/src/option-error.ts +31 -0
  69. package/src/path.ts +288 -0
  70. package/src/pipeline.ts +918 -0
  71. package/src/plugin-routes.ts +188 -0
  72. package/src/plugin.ts +1432 -0
  73. package/src/proxy.ts +236 -0
  74. package/src/query.ts +30 -0
  75. package/src/response.ts +240 -0
  76. package/src/router.ts +119 -0
  77. package/src/security-headers.ts +161 -0
  78. package/src/serialize.ts +264 -0
  79. package/src/size.ts +66 -0
  80. package/src/sse-parse.ts +194 -0
  81. package/src/sse.ts +650 -0
  82. package/src/standard-schema.ts +81 -0
  83. package/src/status.ts +78 -0
  84. package/src/stream.ts +200 -0
  85. package/src/tokens.ts +21 -0
  86. package/src/uploads.ts +176 -0
  87. package/src/utils.ts +31 -0
  88. package/src/validate.ts +65 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Minimal, structural access to the oxc AST nodes exposed by `ClassInfo` / `ArgInfo` (escape
3
+ * hatches of the extension API). The extension does not depend on `oxc-parser`: nodes are read
4
+ * through this loose shape and checked at runtime.
5
+ */
6
+ import type { ClassInfo, MethodInfo, ParameterInfo } from '@znight/compiler'
7
+
8
+ /** An ESTree node, read structurally. */
9
+ export interface AstNode {
10
+ readonly type: string
11
+ readonly start: number
12
+ readonly end: number
13
+ readonly [key: string]: unknown
14
+ }
15
+
16
+ /** `true` for an object shaped like an AST node. */
17
+ export function isNode(value: unknown): value is AstNode {
18
+ return (
19
+ typeof value === 'object' &&
20
+ value !== null &&
21
+ typeof (value as { type?: unknown }).type === 'string' &&
22
+ typeof (value as { start?: unknown }).start === 'number'
23
+ )
24
+ }
25
+
26
+ /** The child node stored under `key`, when it is a node. */
27
+ export function child(node: AstNode, key: string): AstNode | undefined {
28
+ const value = node[key]
29
+ return isNode(value) ? value : undefined
30
+ }
31
+
32
+ /** The child nodes stored under `key`, when it is an array. */
33
+ export function children(node: AstNode, key: string): AstNode[] {
34
+ const value = node[key]
35
+ return Array.isArray(value) ? value.filter(isNode) : []
36
+ }
37
+
38
+ const WRAPPERS = new Set([
39
+ 'ParenthesizedExpression',
40
+ 'TSAsExpression',
41
+ 'TSSatisfiesExpression',
42
+ 'TSNonNullExpression',
43
+ 'TSTypeAssertion',
44
+ 'TSInstantiationExpression',
45
+ ])
46
+
47
+ /** Removes parentheses and type-only wrappers (`x as T`, `x satisfies T`, `x!`, `<T>x`). */
48
+ export function unwrap(node: AstNode): AstNode {
49
+ let current = node
50
+ while (WRAPPERS.has(current.type)) {
51
+ const inner = child(current, 'expression')
52
+ if (!inner) {
53
+ return current
54
+ }
55
+ current = inner
56
+ }
57
+ return current
58
+ }
59
+
60
+ /**
61
+ * The default value expression of a method parameter (`page = 1` → `1`), found in the class
62
+ * declaration by the location of the method key.
63
+ */
64
+ export function defaultValueOf(cls: ClassInfo, method: MethodInfo, param: ParameterInfo): AstNode | undefined {
65
+ const classNode = cls.declaration.node as unknown
66
+ if (!isNode(classNode)) {
67
+ return undefined
68
+ }
69
+ const body = child(classNode, 'body')
70
+ if (!body) {
71
+ return undefined
72
+ }
73
+ for (const member of children(body, 'body')) {
74
+ const key = child(member, 'key')
75
+ if (!key || key.start !== method.location.start) {
76
+ continue
77
+ }
78
+ const fn = child(member, 'value')
79
+ if (!fn) {
80
+ return undefined
81
+ }
82
+ const params = children(fn, 'params').filter(node => !(node.type === 'Identifier' && node['name'] === 'this'))
83
+ let pattern = params[param.index]
84
+ if (pattern?.type === 'TSParameterProperty') {
85
+ pattern = child(pattern, 'parameter')
86
+ }
87
+ return pattern?.type === 'AssignmentPattern' ? child(pattern, 'right') : undefined
88
+ }
89
+ return undefined
90
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Type-based coercion of raw request values (route parameters, query values, headers) to the
3
+ * declared type of a handler parameter (README, "Compiler contract" → `coerce.X` by declared type).
4
+ */
5
+ import type { AnalyzeContext, ClassInfo, MethodInfo, ParameterInfo, TypeInfo } from '@znight/compiler'
6
+ import type { CoercionPlan, HttpControllerData, ParamCoercion, ScalarCoercion } from './model.ts'
7
+
8
+ import { child, defaultValueOf, isNode, unwrap } from './ast.ts'
9
+
10
+ type Context = AnalyzeContext<HttpControllerData>
11
+ type Project = Context['project']
12
+ type ExpressionInput = Parameters<Project['evaluate']>[1]
13
+ type ScopeInput = Parameters<Project['evaluate']>[0]
14
+ type EnumValue = string | number | boolean
15
+
16
+ const DATE_IDENTITY = 'globalThis#Date'
17
+ const STRING: TypeInfo = { kind: 'primitive', name: 'string' }
18
+ const NUMBER: TypeInfo = { kind: 'primitive', name: 'number' }
19
+ const BOOLEAN: TypeInfo = { kind: 'primitive', name: 'boolean' }
20
+ const BIGINT: TypeInfo = { kind: 'primitive', name: 'bigint' }
21
+ const DATE: TypeInfo = { kind: 'reference', identity: DATE_IDENTITY, name: 'Date', typeArgs: [] }
22
+
23
+ /** The type of a handler parameter: its annotation, or the type of its default value. */
24
+ export interface ParameterType {
25
+ /** `undefined` when the parameter has no annotation and its default value tells nothing. */
26
+ readonly type: TypeInfo | undefined
27
+ /** `?`, default value, `| undefined` or `| null`. */
28
+ readonly optional: boolean
29
+ }
30
+
31
+ /** Result of {@link planCoercion}. */
32
+ export type CoercionResult =
33
+ | { readonly ok: true; readonly coercion: ParamCoercion }
34
+ | { readonly ok: false; readonly reason: string; readonly hints: readonly string[] }
35
+
36
+ /** Annotated or inferred type of a parameter. */
37
+ export function parameterType(ctx: Context, cls: ClassInfo, method: MethodInfo, param: ParameterInfo): ParameterType {
38
+ let type: TypeInfo | undefined = param.type
39
+ if (param.type.kind === 'opaque' && param.type.reason === 'missing-annotation') {
40
+ type = param.hasDefault ? inferFromDefault(ctx, cls, method, param) : undefined
41
+ }
42
+ const nullish = type !== undefined && hasNullish(type)
43
+ return { type, optional: param.optional || param.hasDefault || nullish }
44
+ }
45
+
46
+ /**
47
+ * Plans the coercion of a raw value to `type`. `required` is set for route parameters, which are
48
+ * always present (no `coerce.optional`).
49
+ */
50
+ export function planCoercion(
51
+ ctx: Context,
52
+ declared: ParameterType,
53
+ param: ParameterInfo,
54
+ required: boolean,
55
+ ): CoercionResult {
56
+ const type = declared.type
57
+ if (type === undefined) {
58
+ return {
59
+ ok: false,
60
+ reason: `the type of parameter '${param.name}' is unknown`,
61
+ hints: [`Annotate the parameter: ${param.name}: string`],
62
+ }
63
+ }
64
+ const { type: base } = ctx.types.stripNullish(type)
65
+ const raw = isRaw(base)
66
+ if (raw) {
67
+ return { ok: true, coercion: { plan: { kind: 'raw' }, cast: false } }
68
+ }
69
+ const scalar = scalarPlan(ctx, base)
70
+ let plan: CoercionPlan
71
+ let cast: boolean
72
+ if ('reason' in scalar) {
73
+ if (base.kind !== 'array') {
74
+ return failure(ctx, type, scalar.reason)
75
+ }
76
+ const item = scalarPlan(ctx, base.element)
77
+ if ('reason' in item) {
78
+ return failure(ctx, type, item.reason)
79
+ }
80
+ plan = { kind: 'array', item: item.plan }
81
+ cast = item.cast
82
+ } else {
83
+ plan = scalar.plan
84
+ cast = scalar.cast
85
+ }
86
+ if (!required && declared.optional) {
87
+ plan = { kind: 'optional', item: plan }
88
+ // `coerce.optional` returns `T | undefined`: `x: T | null` (without `?` / default) needs a cast
89
+ cast ||= !(param.optional || param.hasDefault || acceptsUndefined(type))
90
+ }
91
+ return { ok: true, coercion: { plan, cast } }
92
+ }
93
+
94
+ /** `unknown` and `any`: the raw value is passed as is. */
95
+ export function isRaw(type: TypeInfo): boolean {
96
+ return type.kind === 'primitive' && (type.name === 'unknown' || type.name === 'any')
97
+ }
98
+
99
+ type ScalarResult =
100
+ | { readonly plan: CoercionPlan; readonly cast: boolean }
101
+ | { readonly reason: string; readonly cast?: undefined }
102
+
103
+ function scalarPlan(ctx: Context, type: TypeInfo): ScalarResult {
104
+ switch (type.kind) {
105
+ case 'primitive': {
106
+ const kind = primitiveCoercion(type.name)
107
+ return kind ? { plan: { kind }, cast: false } : { reason: `'${type.name}' values cannot be read from a request` }
108
+ }
109
+ case 'literal':
110
+ return typeof type.value === 'bigint'
111
+ ? { reason: 'bigint literal types are not supported' }
112
+ : { plan: { kind: 'enum', values: [type.value] }, cast: false }
113
+ case 'union': {
114
+ const values: EnumValue[] = []
115
+ for (const member of type.types) {
116
+ if (member.kind === 'literal' && typeof member.value !== 'bigint') {
117
+ values.push(member.value)
118
+ } else if (member.kind === 'primitive' && member.name === 'boolean') {
119
+ values.push(false, true)
120
+ } else {
121
+ return { reason: 'only unions of literal types can be coerced' }
122
+ }
123
+ }
124
+ return { plan: { kind: 'enum', values }, cast: false }
125
+ }
126
+ case 'reference': {
127
+ if (type.identity === DATE_IDENTITY) {
128
+ return { plan: { kind: 'date' }, cast: false }
129
+ }
130
+ const values = enumValues(ctx, type.identity)
131
+ if (values) {
132
+ // enum members are nominal: the literal values are cast to the enum type
133
+ return values.length === 0
134
+ ? { reason: `the enum ${type.name} has no member` }
135
+ : { plan: { kind: 'enum', values }, cast: true }
136
+ }
137
+ return { reason: 'objects cannot be read from a single request value' }
138
+ }
139
+ case 'intersection': {
140
+ // branded primitives: `string & { readonly __brand: 'UserId' }`
141
+ const primitives = type.types.filter(member => member.kind === 'primitive' || member.kind === 'literal')
142
+ const [primitive] = primitives
143
+ if (primitives.length === 1 && primitive !== undefined) {
144
+ const inner = scalarPlan(ctx, primitive)
145
+ return 'reason' in inner ? inner : { plan: inner.plan, cast: true }
146
+ }
147
+ return { reason: 'only branded primitives (`string & { ... }`) can be coerced' }
148
+ }
149
+ case 'array':
150
+ return { reason: 'nested arrays cannot be coerced' }
151
+ default:
152
+ return { reason: 'this type cannot be read from a request value' }
153
+ }
154
+ }
155
+
156
+ function primitiveCoercion(name: string): ScalarCoercion | undefined {
157
+ switch (name) {
158
+ case 'string':
159
+ case 'number':
160
+ case 'boolean':
161
+ case 'bigint':
162
+ return name
163
+ default:
164
+ return undefined
165
+ }
166
+ }
167
+
168
+ function failure(ctx: Context, type: TypeInfo, reason: string): CoercionResult {
169
+ return {
170
+ ok: false,
171
+ reason: `type '${ctx.types.format(type)}' cannot be coerced (${reason})`,
172
+ hints: [
173
+ 'Supported types: string, number, boolean, bigint, Date, literal unions, enums, branded primitives and arrays of them',
174
+ 'Or validate the value with a Standard Schema',
175
+ ],
176
+ }
177
+ }
178
+
179
+ /** Member values of a TypeScript enum, `undefined` when `identity` is not an evaluable enum. */
180
+ function enumValues(ctx: Context, identity: string): EnumValue[] | undefined {
181
+ const symbol = ctx.project.lookupIdentity(identity)
182
+ if (symbol.kind !== 'declaration' || symbol.declaration.kind !== 'enum') {
183
+ return undefined
184
+ }
185
+ const declaration = symbol.declaration
186
+ const node = declaration.node as unknown
187
+ const id = isNode(node) ? child(node, 'id') : undefined
188
+ const file = ctx.project.getFile(declaration.file)
189
+ if (!id || !file) {
190
+ return undefined
191
+ }
192
+ const scope = { file, namespace: declaration.namespace } as unknown as ScopeInput
193
+ const result = ctx.project.evaluate(scope, id as unknown as ExpressionInput, { refs: 'none' })
194
+ if (result.kind !== 'value' || typeof result.value !== 'object' || result.value === null) {
195
+ return undefined
196
+ }
197
+ return Object.values(result.value).filter(
198
+ (value): value is string | number => typeof value === 'string' || typeof value === 'number',
199
+ )
200
+ }
201
+
202
+ function inferFromDefault(
203
+ ctx: Context,
204
+ cls: ClassInfo,
205
+ method: MethodInfo,
206
+ param: ParameterInfo,
207
+ ): TypeInfo | undefined {
208
+ const node = defaultValueOf(cls, method, param)
209
+ if (!node) {
210
+ return undefined
211
+ }
212
+ const inner = unwrap(node)
213
+ if (inner.type === 'NewExpression') {
214
+ const callee = child(inner, 'callee')
215
+ if (callee?.type === 'Identifier') {
216
+ const resolved = ctx.project.resolveIdentifier(
217
+ cls.file,
218
+ callee as unknown as Parameters<Project['resolveIdentifier']>[1],
219
+ 'value',
220
+ )
221
+ return resolved.identity?.identity === DATE_IDENTITY ? DATE : undefined
222
+ }
223
+ return undefined
224
+ }
225
+ const result = ctx.project.evaluate(cls.file, node as unknown as ExpressionInput, { refs: 'none' })
226
+ return result.kind === 'value' ? typeOfValue(result.value) : undefined
227
+ }
228
+
229
+ function typeOfValue(value: unknown): TypeInfo | undefined {
230
+ switch (typeof value) {
231
+ case 'string':
232
+ return STRING
233
+ case 'number':
234
+ return NUMBER
235
+ case 'boolean':
236
+ return BOOLEAN
237
+ case 'bigint':
238
+ return BIGINT
239
+ default: {
240
+ if (!Array.isArray(value) || value.length === 0) {
241
+ return undefined
242
+ }
243
+ const items = value.map(typeOfValue)
244
+ const [first] = items
245
+ if (first === undefined || first.kind !== 'primitive' || items.some(item => item !== first)) {
246
+ return undefined
247
+ }
248
+ return { kind: 'array', element: first }
249
+ }
250
+ }
251
+ }
252
+
253
+ function hasNullish(type: TypeInfo): boolean {
254
+ return type.kind === 'union' && type.types.some(isNullish)
255
+ }
256
+
257
+ /** Whether a union accepts `undefined` (`T | undefined`, `T | void`). */
258
+ export function acceptsUndefined(type: TypeInfo): boolean {
259
+ return (
260
+ type.kind === 'union' &&
261
+ type.types.some(member => member.kind === 'primitive' && (member.name === 'undefined' || member.name === 'void'))
262
+ )
263
+ }
264
+
265
+ function isNullish(type: TypeInfo): boolean {
266
+ return type.kind === 'primitive' && (type.name === 'undefined' || type.name === 'null' || type.name === 'void')
267
+ }