@typespec/http-server-js 0.58.0-alpha.10-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/build-helpers.ts +170 -0
- package/dist/generated-defs/helpers/header.d.ts +4 -0
- package/dist/generated-defs/helpers/header.d.ts.map +1 -0
- package/dist/generated-defs/helpers/header.js +76 -0
- package/dist/generated-defs/helpers/header.js.map +1 -0
- package/dist/generated-defs/helpers/http.d.ts +4 -0
- package/dist/generated-defs/helpers/http.d.ts.map +1 -0
- package/dist/generated-defs/helpers/http.js +134 -0
- package/dist/generated-defs/helpers/http.js.map +1 -0
- package/dist/generated-defs/helpers/index.d.ts +4 -0
- package/dist/generated-defs/helpers/index.d.ts.map +1 -0
- package/dist/generated-defs/helpers/index.js +21 -0
- package/dist/generated-defs/helpers/index.js.map +1 -0
- package/dist/generated-defs/helpers/multipart.d.ts +4 -0
- package/dist/generated-defs/helpers/multipart.d.ts.map +1 -0
- package/dist/generated-defs/helpers/multipart.js +249 -0
- package/dist/generated-defs/helpers/multipart.js.map +1 -0
- package/dist/generated-defs/helpers/router.d.ts +4 -0
- package/dist/generated-defs/helpers/router.d.ts.map +1 -0
- package/dist/generated-defs/helpers/router.js +259 -0
- package/dist/generated-defs/helpers/router.js.map +1 -0
- package/dist/src/common/declaration.d.ts +13 -0
- package/dist/src/common/declaration.d.ts.map +1 -0
- package/dist/src/common/declaration.js +45 -0
- package/dist/src/common/declaration.js.map +1 -0
- package/dist/src/common/documentation.d.ts +12 -0
- package/dist/src/common/documentation.d.ts.map +1 -0
- package/dist/src/common/documentation.js +21 -0
- package/dist/src/common/documentation.js.map +1 -0
- package/dist/src/common/enum.d.ts +10 -0
- package/dist/src/common/enum.d.ts.map +1 -0
- package/dist/src/common/enum.js +21 -0
- package/dist/src/common/enum.js.map +1 -0
- package/dist/src/common/interface.d.ts +50 -0
- package/dist/src/common/interface.d.ts.map +1 -0
- package/dist/src/common/interface.js +194 -0
- package/dist/src/common/interface.js.map +1 -0
- package/dist/src/common/model.d.ts +26 -0
- package/dist/src/common/model.d.ts.map +1 -0
- package/dist/src/common/model.js +115 -0
- package/dist/src/common/model.js.map +1 -0
- package/dist/src/common/namespace.d.ts +38 -0
- package/dist/src/common/namespace.d.ts.map +1 -0
- package/dist/src/common/namespace.js +184 -0
- package/dist/src/common/namespace.js.map +1 -0
- package/dist/src/common/reference.d.ts +46 -0
- package/dist/src/common/reference.d.ts.map +1 -0
- package/dist/src/common/reference.js +243 -0
- package/dist/src/common/reference.js.map +1 -0
- package/dist/src/common/scalar.d.ts +50 -0
- package/dist/src/common/scalar.d.ts.map +1 -0
- package/dist/src/common/scalar.js +144 -0
- package/dist/src/common/scalar.js.map +1 -0
- package/dist/src/common/serialization/index.d.ts +11 -0
- package/dist/src/common/serialization/index.d.ts.map +1 -0
- package/dist/src/common/serialization/index.js +72 -0
- package/dist/src/common/serialization/index.js.map +1 -0
- package/dist/src/common/serialization/json.d.ts +6 -0
- package/dist/src/common/serialization/json.d.ts.map +1 -0
- package/dist/src/common/serialization/json.js +341 -0
- package/dist/src/common/serialization/json.js.map +1 -0
- package/dist/src/common/union.d.ts +23 -0
- package/dist/src/common/union.d.ts.map +1 -0
- package/dist/src/common/union.js +57 -0
- package/dist/src/common/union.js.map +1 -0
- package/dist/src/ctx.d.ts +242 -0
- package/dist/src/ctx.d.ts.map +1 -0
- package/dist/src/ctx.js +211 -0
- package/dist/src/ctx.js.map +1 -0
- package/dist/src/helpers/header.d.ts +14 -0
- package/dist/src/helpers/header.d.ts.map +1 -0
- package/dist/src/helpers/header.js +38 -0
- package/dist/src/helpers/header.js.map +1 -0
- package/dist/src/helpers/http.d.ts +70 -0
- package/dist/src/helpers/http.d.ts.map +1 -0
- package/dist/src/helpers/http.js +86 -0
- package/dist/src/helpers/http.js.map +1 -0
- package/dist/src/helpers/multipart.d.ts +26 -0
- package/dist/src/helpers/multipart.d.ts.map +1 -0
- package/dist/src/helpers/multipart.js +182 -0
- package/dist/src/helpers/multipart.js.map +1 -0
- package/dist/src/helpers/router.d.ts +176 -0
- package/dist/src/helpers/router.d.ts.map +1 -0
- package/dist/src/helpers/router.js +55 -0
- package/dist/src/helpers/router.js.map +1 -0
- package/dist/src/http/index.d.ts +24 -0
- package/dist/src/http/index.d.ts.map +1 -0
- package/dist/src/http/index.js +52 -0
- package/dist/src/http/index.js.map +1 -0
- package/dist/src/http/server/index.d.ts +11 -0
- package/dist/src/http/server/index.d.ts.map +1 -0
- package/dist/src/http/server/index.js +413 -0
- package/dist/src/http/server/index.js.map +1 -0
- package/dist/src/http/server/multipart.d.ts +16 -0
- package/dist/src/http/server/multipart.d.ts.map +1 -0
- package/dist/src/http/server/multipart.js +214 -0
- package/dist/src/http/server/multipart.js.map +1 -0
- package/dist/src/http/server/router.d.ts +15 -0
- package/dist/src/http/server/router.d.ts.map +1 -0
- package/dist/src/http/server/router.js +459 -0
- package/dist/src/http/server/router.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib.d.ts +141 -0
- package/dist/src/lib.d.ts.map +1 -0
- package/dist/src/lib.js +116 -0
- package/dist/src/lib.js.map +1 -0
- package/dist/src/scripts/scaffold/bin.d.mts +14 -0
- package/dist/src/scripts/scaffold/bin.d.mts.map +1 -0
- package/dist/src/scripts/scaffold/bin.mjs +559 -0
- package/dist/src/scripts/scaffold/bin.mjs.map +1 -0
- package/dist/src/testing/index.d.ts +3 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +6 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/util/case.d.ts +81 -0
- package/dist/src/util/case.d.ts.map +1 -0
- package/dist/src/util/case.js +111 -0
- package/dist/src/util/case.js.map +1 -0
- package/dist/src/util/differentiate.d.ts +251 -0
- package/dist/src/util/differentiate.d.ts.map +1 -0
- package/dist/src/util/differentiate.js +580 -0
- package/dist/src/util/differentiate.js.map +1 -0
- package/dist/src/util/error.d.ts +13 -0
- package/dist/src/util/error.d.ts.map +1 -0
- package/dist/src/util/error.js +25 -0
- package/dist/src/util/error.js.map +1 -0
- package/dist/src/util/extends.d.ts +10 -0
- package/dist/src/util/extends.d.ts.map +1 -0
- package/dist/src/util/extends.js +31 -0
- package/dist/src/util/extends.js.map +1 -0
- package/dist/src/util/iter.d.ts +39 -0
- package/dist/src/util/iter.d.ts.map +1 -0
- package/dist/src/util/iter.js +72 -0
- package/dist/src/util/iter.js.map +1 -0
- package/dist/src/util/keywords.d.ts +10 -0
- package/dist/src/util/keywords.d.ts.map +1 -0
- package/dist/src/util/keywords.js +85 -0
- package/dist/src/util/keywords.js.map +1 -0
- package/dist/src/util/name.d.ts +12 -0
- package/dist/src/util/name.d.ts.map +1 -0
- package/dist/src/util/name.js +26 -0
- package/dist/src/util/name.js.map +1 -0
- package/dist/src/util/once-queue.d.ts +24 -0
- package/dist/src/util/once-queue.d.ts.map +1 -0
- package/dist/src/util/once-queue.js +34 -0
- package/dist/src/util/once-queue.js.map +1 -0
- package/dist/src/util/openapi3.d.ts +23 -0
- package/dist/src/util/openapi3.d.ts.map +1 -0
- package/dist/src/util/openapi3.js +40 -0
- package/dist/src/util/openapi3.js.map +1 -0
- package/dist/src/util/pluralism.d.ts +23 -0
- package/dist/src/util/pluralism.d.ts.map +1 -0
- package/dist/src/util/pluralism.js +36 -0
- package/dist/src/util/pluralism.js.map +1 -0
- package/dist/src/util/scope.d.ts +85 -0
- package/dist/src/util/scope.d.ts.map +1 -0
- package/dist/src/util/scope.js +111 -0
- package/dist/src/util/scope.js.map +1 -0
- package/dist/src/write.d.ts +23 -0
- package/dist/src/write.d.ts.map +1 -0
- package/dist/src/write.js +62 -0
- package/dist/src/write.js.map +1 -0
- package/generated-defs/helpers/header.ts +83 -0
- package/generated-defs/helpers/http.ts +141 -0
- package/generated-defs/helpers/index.ts +27 -0
- package/generated-defs/helpers/multipart.ts +256 -0
- package/generated-defs/helpers/router.ts +266 -0
- package/package.json +71 -0
- package/src/common/declaration.ts +52 -0
- package/src/common/documentation.ts +26 -0
- package/src/common/enum.ts +28 -0
- package/src/common/interface.ts +264 -0
- package/src/common/model.ts +160 -0
- package/src/common/namespace.ts +243 -0
- package/src/common/reference.ts +319 -0
- package/src/common/scalar.ts +173 -0
- package/src/common/serialization/index.ts +124 -0
- package/src/common/serialization/json.ts +444 -0
- package/src/common/union.ts +76 -0
- package/src/ctx.ts +497 -0
- package/src/helpers/header.ts +55 -0
- package/src/helpers/http.ts +113 -0
- package/src/helpers/multipart.ts +228 -0
- package/src/helpers/router.ts +238 -0
- package/src/http/index.ts +81 -0
- package/src/http/server/index.ts +548 -0
- package/src/http/server/multipart.ts +272 -0
- package/src/http/server/router.ts +686 -0
- package/src/index.ts +56 -0
- package/src/lib.ts +130 -0
- package/src/scripts/scaffold/bin.mts +781 -0
- package/src/testing/index.ts +10 -0
- package/src/util/case.ts +182 -0
- package/src/util/differentiate.ts +957 -0
- package/src/util/error.ts +28 -0
- package/src/util/extends.ts +43 -0
- package/src/util/iter.ts +85 -0
- package/src/util/keywords.ts +90 -0
- package/src/util/name.ts +33 -0
- package/src/util/once-queue.ts +55 -0
- package/src/util/openapi3.ts +53 -0
- package/src/util/pluralism.ts +37 -0
- package/src/util/scope.ts +211 -0
- package/src/write.ts +88 -0
- package/temp/tsconfig.tsbuildinfo +1 -0
- package/test/header.test.ts +26 -0
- package/test/multipart.test.ts +169 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +4 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
import { Module } from "../../src/ctx.js";
|
|
5
|
+
|
|
6
|
+
export let module: Module = undefined as any;
|
|
7
|
+
|
|
8
|
+
// prettier-ignore
|
|
9
|
+
const lines = [
|
|
10
|
+
"// Copyright (c) Microsoft Corporation",
|
|
11
|
+
"// Licensed under the MIT license.",
|
|
12
|
+
"",
|
|
13
|
+
"import type * as http from \"node:http\";",
|
|
14
|
+
"",
|
|
15
|
+
"export interface HttpPart {",
|
|
16
|
+
" headers: { [k: string]: string | undefined };",
|
|
17
|
+
" body: ReadableStream<Buffer>;",
|
|
18
|
+
"}",
|
|
19
|
+
"",
|
|
20
|
+
"/**",
|
|
21
|
+
" * Consumes a stream of incoming data and splits it into individual streams for each part of a multipart request, using",
|
|
22
|
+
" * the provided `boundary` value.",
|
|
23
|
+
" */",
|
|
24
|
+
"function MultipartBoundaryTransformStream(",
|
|
25
|
+
" boundary: string,",
|
|
26
|
+
"): ReadableWritablePair<ReadableStream<Buffer>, Buffer> {",
|
|
27
|
+
" let buffer: Buffer = Buffer.alloc(0);",
|
|
28
|
+
" // Initialize subcontroller to an object that does nothing. Multipart bodies may contain a preamble before the first",
|
|
29
|
+
" // boundary, so this dummy controller will discard it.",
|
|
30
|
+
" let subController: { enqueue(chunk: Buffer): void; close(): void } | null = {",
|
|
31
|
+
" enqueue() {},",
|
|
32
|
+
" close() {},",
|
|
33
|
+
" };",
|
|
34
|
+
"",
|
|
35
|
+
" let boundarySplit = Buffer.from(`--${boundary}`);",
|
|
36
|
+
" let initialized = false;",
|
|
37
|
+
"",
|
|
38
|
+
" // We need to keep at least the length of the boundary split plus room for CRLFCRLF in the buffer to detect the boundaries.",
|
|
39
|
+
" // We subtract one from this length because if the whole thing were in the buffer, we would detect it and move past it.",
|
|
40
|
+
" const bufferKeepLength = boundarySplit.length + BUF_CRLFCRLF.length - 1;",
|
|
41
|
+
" let _readableController: ReadableStreamDefaultController<ReadableStream<Buffer>> = null as any;",
|
|
42
|
+
"",
|
|
43
|
+
" const readable = new ReadableStream<ReadableStream<Buffer>>({",
|
|
44
|
+
" start(controller) {",
|
|
45
|
+
" _readableController = controller;",
|
|
46
|
+
" },",
|
|
47
|
+
" });",
|
|
48
|
+
"",
|
|
49
|
+
" const readableController = _readableController;",
|
|
50
|
+
"",
|
|
51
|
+
" const writable = new WritableStream<Buffer>({",
|
|
52
|
+
" write: async (chunk) => {",
|
|
53
|
+
" buffer = Buffer.concat([buffer, chunk]);",
|
|
54
|
+
"",
|
|
55
|
+
" let index: number;",
|
|
56
|
+
"",
|
|
57
|
+
" while ((index = buffer.indexOf(boundarySplit)) !== -1) {",
|
|
58
|
+
" // We found a boundary, emit everything before it and initialize a new stream for the next part.",
|
|
59
|
+
"",
|
|
60
|
+
" // We are initialized if we have found the boundary at least once.",
|
|
61
|
+
" //",
|
|
62
|
+
" // Cases",
|
|
63
|
+
" // 1. If the index is zero and we aren't initialized, there was no preamble.",
|
|
64
|
+
" // 2. If the index is zero and we are initialized, then we had to have found \\r\\n--boundary, nothing special to do.",
|
|
65
|
+
" // 3. If the index is not zero, and we are initialized, then we found \\r\\n--boundary somewhere in the middle,",
|
|
66
|
+
" // nothing special to do.",
|
|
67
|
+
" // 4. If the index is not zero and we aren't initialized, then we need to check that boundarySplit was preceded",
|
|
68
|
+
" // by \\r\\n for validity, because the preamble must end with \\r\\n.",
|
|
69
|
+
"",
|
|
70
|
+
" if (index > 0) {",
|
|
71
|
+
" if (!initialized) {",
|
|
72
|
+
" if (!buffer.subarray(index - 2, index).equals(Buffer.from(\"\\r\\n\"))) {",
|
|
73
|
+
" readableController.error(new Error(\"Invalid preamble in multipart body.\"));",
|
|
74
|
+
" } else {",
|
|
75
|
+
" await enqueueSub(buffer.subarray(0, index - 2));",
|
|
76
|
+
" }",
|
|
77
|
+
" } else {",
|
|
78
|
+
" await enqueueSub(buffer.subarray(0, index));",
|
|
79
|
+
" }",
|
|
80
|
+
" }",
|
|
81
|
+
"",
|
|
82
|
+
" // We enqueued everything before the boundary, so we clear the buffer past the boundary",
|
|
83
|
+
" buffer = buffer.subarray(index + boundarySplit.length);",
|
|
84
|
+
"",
|
|
85
|
+
" // We're done with the current part, so close the stream. If this is the opening boundary, there won't be a",
|
|
86
|
+
" // subcontroller yet.",
|
|
87
|
+
" subController?.close();",
|
|
88
|
+
" subController = null;",
|
|
89
|
+
"",
|
|
90
|
+
" if (!initialized) {",
|
|
91
|
+
" initialized = true;",
|
|
92
|
+
" boundarySplit = Buffer.from(`\\r\\n${boundarySplit}`);",
|
|
93
|
+
" }",
|
|
94
|
+
" }",
|
|
95
|
+
"",
|
|
96
|
+
" if (buffer.length > bufferKeepLength) {",
|
|
97
|
+
" await enqueueSub(buffer.subarray(0, -bufferKeepLength));",
|
|
98
|
+
" buffer = buffer.subarray(-bufferKeepLength);",
|
|
99
|
+
" }",
|
|
100
|
+
" },",
|
|
101
|
+
" close() {",
|
|
102
|
+
" if (!/--(\\r\\n)?/.test(buffer.toString(\"utf-8\"))) {",
|
|
103
|
+
" readableController.error(new Error(\"Unexpected characters after final boundary.\"));",
|
|
104
|
+
" }",
|
|
105
|
+
"",
|
|
106
|
+
" subController?.close();",
|
|
107
|
+
"",
|
|
108
|
+
" readableController.close();",
|
|
109
|
+
" },",
|
|
110
|
+
" });",
|
|
111
|
+
"",
|
|
112
|
+
" async function enqueueSub(s: Buffer) {",
|
|
113
|
+
" subController ??= await new Promise<ReadableStreamDefaultController>((resolve) => {",
|
|
114
|
+
" readableController.enqueue(",
|
|
115
|
+
" new ReadableStream<Buffer>({",
|
|
116
|
+
" start: (controller) => resolve(controller),",
|
|
117
|
+
" }),",
|
|
118
|
+
" );",
|
|
119
|
+
" });",
|
|
120
|
+
"",
|
|
121
|
+
" subController.enqueue(s);",
|
|
122
|
+
" }",
|
|
123
|
+
"",
|
|
124
|
+
" return { readable, writable };",
|
|
125
|
+
"}",
|
|
126
|
+
"",
|
|
127
|
+
"const BUF_CRLFCRLF = Buffer.from(\"\\r\\n\\r\\n\");",
|
|
128
|
+
"",
|
|
129
|
+
"/**",
|
|
130
|
+
" * Consumes a stream of the contents of a single part of a multipart request and emits an `HttpPart` object for each part.",
|
|
131
|
+
" * This consumes just enough of the stream to read the headers, and then forwards the rest of the stream as the body.",
|
|
132
|
+
" */",
|
|
133
|
+
"class HttpPartTransform extends TransformStream<ReadableStream<Buffer>, HttpPart> {",
|
|
134
|
+
" constructor() {",
|
|
135
|
+
" super({",
|
|
136
|
+
" transform: async (partRaw, controller) => {",
|
|
137
|
+
" const reader = partRaw.getReader();",
|
|
138
|
+
"",
|
|
139
|
+
" let buf = Buffer.alloc(0);",
|
|
140
|
+
" let idx;",
|
|
141
|
+
"",
|
|
142
|
+
" while ((idx = buf.indexOf(BUF_CRLFCRLF)) === -1) {",
|
|
143
|
+
" const { done, value } = await reader.read();",
|
|
144
|
+
" if (done) {",
|
|
145
|
+
" throw new Error(\"Unexpected end of part.\");",
|
|
146
|
+
" }",
|
|
147
|
+
" buf = Buffer.concat([buf, value]);",
|
|
148
|
+
" }",
|
|
149
|
+
"",
|
|
150
|
+
" const headerText = buf.subarray(0, idx).toString(\"utf-8\").trim();",
|
|
151
|
+
"",
|
|
152
|
+
" const headers = Object.fromEntries(",
|
|
153
|
+
" headerText.split(\"\\r\\n\").map((line) => {",
|
|
154
|
+
" const [name, value] = line.split(\": \", 2);",
|
|
155
|
+
"",
|
|
156
|
+
" return [name.toLowerCase(), value];",
|
|
157
|
+
" }),",
|
|
158
|
+
" ) as { [k: string]: string };",
|
|
159
|
+
"",
|
|
160
|
+
" const body = new ReadableStream<Buffer>({",
|
|
161
|
+
" start(controller) {",
|
|
162
|
+
" controller.enqueue(buf.subarray(idx + BUF_CRLFCRLF.length));",
|
|
163
|
+
" },",
|
|
164
|
+
" async pull(controller) {",
|
|
165
|
+
" const { done, value } = await reader.read();",
|
|
166
|
+
"",
|
|
167
|
+
" if (done) {",
|
|
168
|
+
" controller.close();",
|
|
169
|
+
" } else {",
|
|
170
|
+
" controller.enqueue(value);",
|
|
171
|
+
" }",
|
|
172
|
+
" },",
|
|
173
|
+
" });",
|
|
174
|
+
"",
|
|
175
|
+
" controller.enqueue({ headers, body });",
|
|
176
|
+
" },",
|
|
177
|
+
" });",
|
|
178
|
+
" }",
|
|
179
|
+
"}",
|
|
180
|
+
"",
|
|
181
|
+
"/**",
|
|
182
|
+
" * Processes a request as a multipart request, returning a stream of `HttpPart` objects, each representing an individual",
|
|
183
|
+
" * part in the multipart request.",
|
|
184
|
+
" *",
|
|
185
|
+
" * Only call this function if you have already validated the content type of the request and confirmed that it is a",
|
|
186
|
+
" * multipart request.",
|
|
187
|
+
" *",
|
|
188
|
+
" * @throws Error if the content-type header is missing or does not contain a boundary field.",
|
|
189
|
+
" *",
|
|
190
|
+
" * @param request - the incoming request to parse as multipart",
|
|
191
|
+
" * @returns a stream of HttpPart objects, each representing an individual part in the multipart request",
|
|
192
|
+
" */",
|
|
193
|
+
"export function createMultipartReadable(request: http.IncomingMessage): ReadableStream<HttpPart> {",
|
|
194
|
+
" const boundary = request.headers[\"content-type\"]",
|
|
195
|
+
" ?.split(\";\")",
|
|
196
|
+
" .find((s) => s.includes(\"boundary=\"))",
|
|
197
|
+
" ?.split(\"=\", 2)[1];",
|
|
198
|
+
" if (!boundary) {",
|
|
199
|
+
" throw new Error(\"Invalid request: missing boundary in content-type.\");",
|
|
200
|
+
" }",
|
|
201
|
+
"",
|
|
202
|
+
" const bodyStream = new ReadableStream<Uint8Array>({",
|
|
203
|
+
" start(controller) {",
|
|
204
|
+
" request.on(\"data\", (chunk: Buffer) => {",
|
|
205
|
+
" controller.enqueue(chunk);",
|
|
206
|
+
" });",
|
|
207
|
+
" request.on(\"end\", () => controller.close());",
|
|
208
|
+
" },",
|
|
209
|
+
" });",
|
|
210
|
+
"",
|
|
211
|
+
" return bodyStream",
|
|
212
|
+
" .pipeThrough(MultipartBoundaryTransformStream(boundary))",
|
|
213
|
+
" .pipeThrough(new HttpPartTransform());",
|
|
214
|
+
"}",
|
|
215
|
+
"",
|
|
216
|
+
"// Gross polyfill because Safari doesn't support this yet.",
|
|
217
|
+
"//",
|
|
218
|
+
"// https://bugs.webkit.org/show_bug.cgi?id=194379",
|
|
219
|
+
"// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#browser_compatibility",
|
|
220
|
+
"(ReadableStream.prototype as any)[Symbol.asyncIterator] ??= async function* () {",
|
|
221
|
+
" const reader = this.getReader();",
|
|
222
|
+
" try {",
|
|
223
|
+
" while (true) {",
|
|
224
|
+
" const { done, value } = await reader.read();",
|
|
225
|
+
" if (done) return value;",
|
|
226
|
+
" yield value;",
|
|
227
|
+
" }",
|
|
228
|
+
" } finally {",
|
|
229
|
+
" reader.releaseLock();",
|
|
230
|
+
" }",
|
|
231
|
+
"};",
|
|
232
|
+
"",
|
|
233
|
+
"declare global {",
|
|
234
|
+
" interface ReadableStream<R> {",
|
|
235
|
+
" [Symbol.asyncIterator](): AsyncIterableIterator<R>;",
|
|
236
|
+
" }",
|
|
237
|
+
"}",
|
|
238
|
+
"",
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
export async function createModule(parent: Module): Promise<Module> {
|
|
242
|
+
if (module) return module;
|
|
243
|
+
|
|
244
|
+
module = {
|
|
245
|
+
name: "multipart",
|
|
246
|
+
cursor: parent.cursor.enter("multipart"),
|
|
247
|
+
imports: [],
|
|
248
|
+
declarations: [],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
module.declarations.push(lines);
|
|
252
|
+
|
|
253
|
+
parent.declarations.push(module);
|
|
254
|
+
|
|
255
|
+
return module;
|
|
256
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
import { Module } from "../../src/ctx.js";
|
|
5
|
+
|
|
6
|
+
export let module: Module = undefined as any;
|
|
7
|
+
|
|
8
|
+
// prettier-ignore
|
|
9
|
+
const lines = [
|
|
10
|
+
"// Copyright (c) Microsoft Corporation",
|
|
11
|
+
"// Licensed under the MIT license.",
|
|
12
|
+
"",
|
|
13
|
+
"import type * as http from \"node:http\";",
|
|
14
|
+
"",
|
|
15
|
+
"/** A policy that can be applied to a route or a set of routes. */",
|
|
16
|
+
"export interface Policy {",
|
|
17
|
+
" /** Optional policy name. */",
|
|
18
|
+
" name?: string;",
|
|
19
|
+
"",
|
|
20
|
+
" /**",
|
|
21
|
+
" * Applies the policy to the request.",
|
|
22
|
+
" *",
|
|
23
|
+
" * Policies _MUST_ call `next()` to pass the request to the next policy _OR_ call `response.end()` to terminate,",
|
|
24
|
+
" * and _MUST NOT_ do both.",
|
|
25
|
+
" *",
|
|
26
|
+
" * If the policy passes a `request` object to `next()`, that request object will be used instead of the original",
|
|
27
|
+
" * request object for the remainder of the policy chain. If the policy does _not_ pass a request object to `next()`,",
|
|
28
|
+
" * the same object that was passed to this policy will be forwarded to the next policy automatically.",
|
|
29
|
+
" *",
|
|
30
|
+
" * @param request - The incoming HTTP request.",
|
|
31
|
+
" * @param response - The outgoing HTTP response.",
|
|
32
|
+
" * @param next - Calls the next policy in the chain.",
|
|
33
|
+
" */",
|
|
34
|
+
" (ctx: HttpContext, next: (request?: http.IncomingMessage) => void): void;",
|
|
35
|
+
"}",
|
|
36
|
+
"",
|
|
37
|
+
"/**",
|
|
38
|
+
" * Create a function from a chain of policies.",
|
|
39
|
+
" *",
|
|
40
|
+
" * This returns a single function that will apply the policy chain and eventually call the provided `next()` function.",
|
|
41
|
+
" *",
|
|
42
|
+
" * @param name - The name to give to the policy chain function.",
|
|
43
|
+
" * @param policies - The policies to apply to the request.",
|
|
44
|
+
" * @param out - The function to call after the policies have been applied.",
|
|
45
|
+
" */",
|
|
46
|
+
"export function createPolicyChain<Out extends (ctx: HttpContext, ...rest: any[]) => void>(",
|
|
47
|
+
" name: string,",
|
|
48
|
+
" policies: Policy[],",
|
|
49
|
+
" out: Out,",
|
|
50
|
+
"): Out {",
|
|
51
|
+
" let outParams: any[];",
|
|
52
|
+
" if (policies.length === 0) {",
|
|
53
|
+
" return out;",
|
|
54
|
+
" }",
|
|
55
|
+
"",
|
|
56
|
+
" function applyPolicy(ctx: HttpContext, index: number) {",
|
|
57
|
+
" if (index >= policies.length) {",
|
|
58
|
+
" return out(ctx, ...outParams);",
|
|
59
|
+
" }",
|
|
60
|
+
"",
|
|
61
|
+
" policies[index](ctx, function nextPolicy(nextRequest) {",
|
|
62
|
+
" applyPolicy(",
|
|
63
|
+
" {",
|
|
64
|
+
" ...ctx,",
|
|
65
|
+
" request: nextRequest ?? ctx.request,",
|
|
66
|
+
" },",
|
|
67
|
+
" index + 1,",
|
|
68
|
+
" );",
|
|
69
|
+
" });",
|
|
70
|
+
" }",
|
|
71
|
+
"",
|
|
72
|
+
" return {",
|
|
73
|
+
" [name](ctx: HttpContext, ...params: any[]) {",
|
|
74
|
+
" outParams = params;",
|
|
75
|
+
" applyPolicy(ctx, 0);",
|
|
76
|
+
" },",
|
|
77
|
+
" }[name] as Out;",
|
|
78
|
+
"}",
|
|
79
|
+
"",
|
|
80
|
+
"/**",
|
|
81
|
+
" * The type of an error encountered during request validation.",
|
|
82
|
+
" */",
|
|
83
|
+
"export type ValidationError = string;",
|
|
84
|
+
"",
|
|
85
|
+
"/**",
|
|
86
|
+
" * An object specifying the policies for a given route configuration.",
|
|
87
|
+
" */",
|
|
88
|
+
"export type RoutePolicies<RouteConfig extends { [k: string]: object }> = {",
|
|
89
|
+
" [Interface in keyof RouteConfig]?: {",
|
|
90
|
+
" before?: Policy[];",
|
|
91
|
+
" after?: Policy[];",
|
|
92
|
+
" methodPolicies?: {",
|
|
93
|
+
" [Method in keyof RouteConfig[Interface]]?: Policy[];",
|
|
94
|
+
" };",
|
|
95
|
+
" };",
|
|
96
|
+
"};",
|
|
97
|
+
"",
|
|
98
|
+
"/**",
|
|
99
|
+
" * Create a policy chain for a given route.",
|
|
100
|
+
" *",
|
|
101
|
+
" * This function calls `createPolicyChain` internally and orders the policies based on the route configuration.",
|
|
102
|
+
" *",
|
|
103
|
+
" * Interface-level `before` policies run first, then method-level policies, then Interface-level `after` policies.",
|
|
104
|
+
" *",
|
|
105
|
+
" * @param name - The name to give to the policy chain function.",
|
|
106
|
+
" * @param routePolicies - The policies to apply to the routes (part of the route configuration).",
|
|
107
|
+
" * @param interfaceName - The name of the interface that the route belongs to.",
|
|
108
|
+
" * @param methodName - The name of the method that the route corresponds to.",
|
|
109
|
+
" * @param out - The function to call after the policies have been applied.",
|
|
110
|
+
" */",
|
|
111
|
+
"export function createPolicyChainForRoute<",
|
|
112
|
+
" RouteConfig extends { [k: string]: object },",
|
|
113
|
+
" InterfaceName extends keyof RouteConfig,",
|
|
114
|
+
" Out extends (ctx: HttpContext, ...rest: any[]) => void,",
|
|
115
|
+
">(",
|
|
116
|
+
" name: string,",
|
|
117
|
+
" routePolicies: RoutePolicies<RouteConfig>,",
|
|
118
|
+
" interfaceName: InterfaceName,",
|
|
119
|
+
" methodName: keyof RouteConfig[InterfaceName],",
|
|
120
|
+
" out: Out,",
|
|
121
|
+
"): Out {",
|
|
122
|
+
" return createPolicyChain(",
|
|
123
|
+
" name,",
|
|
124
|
+
" [",
|
|
125
|
+
" ...(routePolicies[interfaceName]?.before ?? []),",
|
|
126
|
+
" ...(routePolicies[interfaceName]?.methodPolicies?.[methodName] ?? []),",
|
|
127
|
+
" ...(routePolicies[interfaceName]?.after ?? []),",
|
|
128
|
+
" ],",
|
|
129
|
+
" out,",
|
|
130
|
+
" );",
|
|
131
|
+
"}",
|
|
132
|
+
"",
|
|
133
|
+
"/**",
|
|
134
|
+
" * Options for configuring a router with additional functionality.",
|
|
135
|
+
" */",
|
|
136
|
+
"export interface RouterOptions<",
|
|
137
|
+
" RouteConfig extends { [k: string]: object } = { [k: string]: object },",
|
|
138
|
+
"> {",
|
|
139
|
+
" /**",
|
|
140
|
+
" * The base path of the router.",
|
|
141
|
+
" *",
|
|
142
|
+
" * This should include any leading slashes, but not a trailing slash, and should not include any component",
|
|
143
|
+
" * of the URL authority (e.g. the scheme, host, or port).",
|
|
144
|
+
" *",
|
|
145
|
+
" * Defaults to \"\".",
|
|
146
|
+
" */",
|
|
147
|
+
" basePath?: string;",
|
|
148
|
+
"",
|
|
149
|
+
" /**",
|
|
150
|
+
" * A list of policies to apply to all routes _before_ routing.",
|
|
151
|
+
" *",
|
|
152
|
+
" * Policies are applied in the order they are listed.",
|
|
153
|
+
" *",
|
|
154
|
+
" * By default, the policy list is empty.",
|
|
155
|
+
" *",
|
|
156
|
+
" * Policies _MUST_ call `next()` to pass the request to the next policy _OR_ call `response.end()` to terminate",
|
|
157
|
+
" * the response and _MUST NOT_ do both.",
|
|
158
|
+
" */",
|
|
159
|
+
" policies?: Policy[];",
|
|
160
|
+
"",
|
|
161
|
+
" /**",
|
|
162
|
+
" * A record of policies that apply to specific routes.",
|
|
163
|
+
" *",
|
|
164
|
+
" * The policies are provided as a nested record where the keys are the business-logic interface names, and the values",
|
|
165
|
+
" * are records of the method names in the given interface and the policies that apply to them.",
|
|
166
|
+
" *",
|
|
167
|
+
" * By default, no additional policies are applied to the routes.",
|
|
168
|
+
" *",
|
|
169
|
+
" * Policies _MUST_ call `next()` to pass the request to the next policy _OR_ call `response.end()` to terminate",
|
|
170
|
+
" * the response and _MUST NOT_ do both.",
|
|
171
|
+
" */",
|
|
172
|
+
" routePolicies?: RoutePolicies<RouteConfig>;",
|
|
173
|
+
"",
|
|
174
|
+
" /**",
|
|
175
|
+
" * A handler for requests where the resource is not found.",
|
|
176
|
+
" *",
|
|
177
|
+
" * The router will call this function when no route matches the incoming request.",
|
|
178
|
+
" *",
|
|
179
|
+
" * If this handler is not provided, a 404 Not Found response with a text body will be returned.",
|
|
180
|
+
" *",
|
|
181
|
+
" * You _MUST_ call `response.end()` to terminate the response.",
|
|
182
|
+
" *",
|
|
183
|
+
" * This handler is unreachable when using the Express middleware, as it will forward non-matching requests to the",
|
|
184
|
+
" * next middleware layer in the stack.",
|
|
185
|
+
" *",
|
|
186
|
+
" * @param ctx - The HTTP context for the request.",
|
|
187
|
+
" */",
|
|
188
|
+
" onRequestNotFound?: (ctx: HttpContext) => void;",
|
|
189
|
+
"",
|
|
190
|
+
" /**",
|
|
191
|
+
" * A handler for requests that fail to validate inputs.",
|
|
192
|
+
" *",
|
|
193
|
+
" * If this handler is not provided, a 400 Bad Request response with a JSON body containing some basic information",
|
|
194
|
+
" * about the error will be returned to the client.",
|
|
195
|
+
" *",
|
|
196
|
+
" * You _MUST_ call `response.end()` to terminate the response.",
|
|
197
|
+
" *",
|
|
198
|
+
" * @param ctx - The HTTP context for the request.",
|
|
199
|
+
" * @param route - The route that was matched.",
|
|
200
|
+
" * @param error - The validation error that was thrown.",
|
|
201
|
+
" */",
|
|
202
|
+
" onInvalidRequest?: (ctx: HttpContext, route: string, error: ValidationError) => void;",
|
|
203
|
+
"",
|
|
204
|
+
" /**",
|
|
205
|
+
" * A handler for requests that throw an error during processing.",
|
|
206
|
+
" *",
|
|
207
|
+
" * If this handler is not provided, a 500 Internal Server Error response with a text body and no error details will be",
|
|
208
|
+
" * returned to the client.",
|
|
209
|
+
" *",
|
|
210
|
+
" * You _MUST_ call `response.end()` to terminate the response.",
|
|
211
|
+
" *",
|
|
212
|
+
" * If this handler itself throws an Error, the router will respond with a 500 Internal Server Error",
|
|
213
|
+
" *",
|
|
214
|
+
" * @param ctx - The HTTP context for the request.",
|
|
215
|
+
" * @param error - The error that was thrown.",
|
|
216
|
+
" */",
|
|
217
|
+
" onInternalError?(ctx: HttpContext, error: Error): void;",
|
|
218
|
+
"}",
|
|
219
|
+
"",
|
|
220
|
+
"/** Context information for operations carried over the HTTP protocol. */",
|
|
221
|
+
"export interface HttpContext {",
|
|
222
|
+
" /** The incoming request to the server. */",
|
|
223
|
+
" request: http.IncomingMessage;",
|
|
224
|
+
" /** The outgoing response object. */",
|
|
225
|
+
" response: http.ServerResponse;",
|
|
226
|
+
"",
|
|
227
|
+
" /**",
|
|
228
|
+
" * Error handling functions provided by the HTTP router. Service implementations may call these methods in case a",
|
|
229
|
+
" * resource is not found, a request is invalid, or an internal error occurs.",
|
|
230
|
+
" *",
|
|
231
|
+
" * These methods will respond to the client with the appropriate status code and message.",
|
|
232
|
+
" */",
|
|
233
|
+
" errorHandlers: {",
|
|
234
|
+
" /**",
|
|
235
|
+
" * Signals that the requested resource was not found.",
|
|
236
|
+
" */",
|
|
237
|
+
" onRequestNotFound: Exclude<RouterOptions[\"onRequestNotFound\"], undefined>;",
|
|
238
|
+
" /**",
|
|
239
|
+
" * Signals that the request was invalid.",
|
|
240
|
+
" */",
|
|
241
|
+
" onInvalidRequest: Exclude<RouterOptions[\"onInvalidRequest\"], undefined>;",
|
|
242
|
+
" /**",
|
|
243
|
+
" * Signals that an internal error occurred.",
|
|
244
|
+
" */",
|
|
245
|
+
" onInternalError: Exclude<RouterOptions[\"onInternalError\"], undefined>;",
|
|
246
|
+
" };",
|
|
247
|
+
"}",
|
|
248
|
+
"",
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
export async function createModule(parent: Module): Promise<Module> {
|
|
252
|
+
if (module) return module;
|
|
253
|
+
|
|
254
|
+
module = {
|
|
255
|
+
name: "router",
|
|
256
|
+
cursor: parent.cursor.enter("router"),
|
|
257
|
+
imports: [],
|
|
258
|
+
declarations: [],
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
module.declarations.push(lines);
|
|
262
|
+
|
|
263
|
+
parent.declarations.push(module);
|
|
264
|
+
|
|
265
|
+
return module;
|
|
266
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typespec/http-server-js",
|
|
3
|
+
"version": "0.58.0-alpha.10-dev.3",
|
|
4
|
+
"author": "Microsoft Corporation",
|
|
5
|
+
"description": "TypeSpec HTTP server code generator for JavaScript",
|
|
6
|
+
"homepage": "https://github.com/microsoft/typespec",
|
|
7
|
+
"readme": "https://github.com/microsoft/typespec/blob/main/packages/http-server-js/README.md",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/microsoft/typespec.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/microsoft/typespec/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"typespec",
|
|
18
|
+
"http",
|
|
19
|
+
"server",
|
|
20
|
+
"javascript",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "dist/src/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./dist/src/index.js",
|
|
27
|
+
"./testing": "./dist/src/testing/index.js"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"hsj-scaffold": "./dist/src/scripts/scaffold/bin.mjs"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@typespec/compiler": "~0.65.3 || >=0.66.0-dev <0.66.0",
|
|
34
|
+
"@typespec/http": "~0.65.0 || >=0.66.0-dev <0.66.0",
|
|
35
|
+
"@typespec/openapi3": "~0.65.0 || >=0.66.0-dev <0.66.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@typespec/openapi3": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"prettier": "~3.4.2",
|
|
44
|
+
"yaml": "~2.7.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "~22.10.10",
|
|
48
|
+
"@typespec/compiler": "~0.65.3 || >=0.66.0-dev <0.66.0",
|
|
49
|
+
"@typespec/http": "~0.65.0 || >=0.66.0-dev <0.66.0",
|
|
50
|
+
"@typespec/openapi3": "~0.65.0 || >=0.66.0-dev <0.66.0",
|
|
51
|
+
"@vitest/coverage-v8": "^3.0.4",
|
|
52
|
+
"@vitest/ui": "^3.0.3",
|
|
53
|
+
"tsx": "^4.19.2",
|
|
54
|
+
"typescript": "~5.7.3",
|
|
55
|
+
"vitest": "^3.0.5"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"clean": "rimraf ./dist ./temp",
|
|
59
|
+
"build": "npm run build:helpers && npm run build:src",
|
|
60
|
+
"build:src": "tsc -p ./tsconfig.json",
|
|
61
|
+
"build:helpers": "tsx ./build-helpers.ts",
|
|
62
|
+
"watch": "tsc -p . --watch",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest -w",
|
|
65
|
+
"test:ui": "vitest --ui",
|
|
66
|
+
"test:ci": "vitest run --coverage --reporter=junit --reporter=default",
|
|
67
|
+
"lint": "eslint . --max-warnings=0",
|
|
68
|
+
"lint:fix": "eslint . --fix",
|
|
69
|
+
"regen-docs": "echo Doc generation disabled for this package."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
import { DeclarationType, JsContext, Module } from "../ctx.js";
|
|
5
|
+
import { emitEnum } from "./enum.js";
|
|
6
|
+
import { emitInterface } from "./interface.js";
|
|
7
|
+
import { emitModel } from "./model.js";
|
|
8
|
+
import { emitScalar } from "./scalar.js";
|
|
9
|
+
import { emitUnion } from "./union.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Emit a declaration for a module based on its type.
|
|
13
|
+
*
|
|
14
|
+
* The altName is optional and is only used for unnamed models and unions.
|
|
15
|
+
*
|
|
16
|
+
* @param ctx - The emitter context.
|
|
17
|
+
* @param type - The type to emit.
|
|
18
|
+
* @param module - The module that this declaration is written into.
|
|
19
|
+
* @param altName - An alternative name to use for the declaration if it is not named.
|
|
20
|
+
*/
|
|
21
|
+
export function* emitDeclaration(
|
|
22
|
+
ctx: JsContext,
|
|
23
|
+
type: DeclarationType,
|
|
24
|
+
module: Module,
|
|
25
|
+
altName?: string,
|
|
26
|
+
): Iterable<string> {
|
|
27
|
+
switch (type.kind) {
|
|
28
|
+
case "Model": {
|
|
29
|
+
yield* emitModel(ctx, type, module, altName);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case "Enum": {
|
|
33
|
+
yield* emitEnum(ctx, type);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case "Union": {
|
|
37
|
+
yield* emitUnion(ctx, type, module, altName);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "Interface": {
|
|
41
|
+
yield* emitInterface(ctx, type, module);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "Scalar": {
|
|
45
|
+
yield emitScalar(ctx, type);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
default: {
|
|
49
|
+
throw new Error(`UNREACHABLE: Unhandled type kind: ${(type satisfies never as any).kind}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
import { Type, getDoc } from "@typespec/compiler";
|
|
5
|
+
import { JsContext } from "../ctx.js";
|
|
6
|
+
import { indent } from "../util/iter.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Emit the documentation for a type in JSDoc format.
|
|
10
|
+
*
|
|
11
|
+
* This assumes that the documentation may include Markdown formatting.
|
|
12
|
+
*
|
|
13
|
+
* @param ctx - The emitter context.
|
|
14
|
+
* @param type - The type to emit documentation for.
|
|
15
|
+
*/
|
|
16
|
+
export function* emitDocumentation(ctx: JsContext, type: Type): Iterable<string> {
|
|
17
|
+
const doc = getDoc(ctx.program, type);
|
|
18
|
+
|
|
19
|
+
if (doc === undefined) return;
|
|
20
|
+
|
|
21
|
+
yield `/**`;
|
|
22
|
+
|
|
23
|
+
yield* indent(doc.trim().split(/\r?\n/g), " * ");
|
|
24
|
+
|
|
25
|
+
yield ` */`;
|
|
26
|
+
}
|