@typespec/http-server-js 0.58.0-alpha.12-dev.1 → 0.58.0-alpha.12-dev.2

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/.testignore ADDED
@@ -0,0 +1,26 @@
1
+ # Failed on initial run
2
+ special-words
3
+ authentication/oauth2
4
+ authentication/union
5
+ encode/bytes
6
+ encode/datetime
7
+ encode/duration
8
+ encode/numeric
9
+ payload/content-negotiation
10
+ payload/media-type
11
+ payload/multipart
12
+ payload/xml
13
+ response/status-code-range
14
+ streaming/jsonl
15
+ type/array
16
+ type/dictionary
17
+ type/enum/extensible
18
+ type/enum/fixed
19
+ type/model/inheritance/enum-discriminator
20
+ type/property/additional-properties
21
+ type/property/nullable
22
+ type/property/optionality
23
+ type/property/value-types
24
+ type/scalar
25
+ type/union
26
+ versioning/returnTypeChangedFrom
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ import { run } from "@typespec/internal-build-utils";
4
+ import pkg from "fs-extra";
5
+ import { copyFile, mkdir, rm } from "fs/promises";
6
+ import { globby } from "globby";
7
+ import inquirer from "inquirer";
8
+ import ora from "ora";
9
+ import pLimit from "p-limit";
10
+ import { basename, dirname, join, resolve } from "path";
11
+ import pc from "picocolors";
12
+ import { fileURLToPath } from "url";
13
+ import { hideBin } from "yargs/helpers";
14
+ import yargs from "yargs/yargs";
15
+
16
+ const { pathExists, stat, readFile, writeFile } = pkg;
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ const projectRoot = join(__dirname, "../..");
22
+ const tspConfig = join(__dirname, "tspconfig.yaml");
23
+
24
+ const basePath = join(projectRoot, "node_modules", "@typespec", "http-specs", "specs");
25
+ const ignoreFilePath = join(projectRoot, ".testignore");
26
+ const logDirRoot = join(projectRoot, "temp", "emit-e2e-logs");
27
+ const reportFilePath = join(logDirRoot, "report.txt");
28
+
29
+ // Remove the log directory if it exists.
30
+ async function clearLogDirectory() {
31
+ if (await pathExists(logDirRoot)) {
32
+ await rm(logDirRoot, { recursive: true, force: true });
33
+ }
34
+ }
35
+
36
+ // Parse command-line arguments.
37
+ const argv = yargs(hideBin(process.argv))
38
+ .option("interactive", {
39
+ type: "boolean",
40
+ describe: "Enable interactive mode",
41
+ default: false,
42
+ })
43
+ .positional("paths", {
44
+ describe: "Optional list of specific file or directory paths to process (relative to basePath)",
45
+ type: "string",
46
+ array: true,
47
+ default: [],
48
+ })
49
+ .option("build", {
50
+ type: "boolean",
51
+ describe: "Build the generated projects",
52
+ default: false,
53
+ })
54
+ .help().argv;
55
+
56
+ // Read and parse the ignore file.
57
+ async function getIgnoreList() {
58
+ try {
59
+ const content = await readFile(ignoreFilePath, "utf8");
60
+ return content
61
+ .split(/\r?\n/)
62
+ .filter((line) => line.trim() && !line.startsWith("#"))
63
+ .map((line) => line.trim());
64
+ } catch {
65
+ console.warn(pc.yellow("No ignore file found."));
66
+ return [];
67
+ }
68
+ }
69
+
70
+ // Recursively process paths (files or directories relative to basePath).
71
+ async function processPaths(paths, ignoreList) {
72
+ const results = [];
73
+ for (const relativePath of paths) {
74
+ const fullPath = resolve(basePath, relativePath);
75
+
76
+ if (!(await pathExists(fullPath))) {
77
+ console.warn(pc.yellow(`Path not found: ${relativePath}`));
78
+ continue;
79
+ }
80
+
81
+ const stats = await stat(fullPath);
82
+ if (stats.isFile() && fullPath.endsWith("main.tsp")) {
83
+ if (ignoreList.some((ignore) => relativePath.startsWith(ignore))) continue;
84
+ results.push({ fullPath, relativePath });
85
+ } else if (stats.isDirectory()) {
86
+ const patterns = ["**/main.tsp"];
87
+ const discoveredPaths = await globby(patterns, { cwd: fullPath });
88
+ const validFiles = discoveredPaths
89
+ .map((p) => ({
90
+ fullPath: join(fullPath, p),
91
+ relativePath: join(relativePath, p),
92
+ }))
93
+ .filter((file) => !ignoreList.some((ignore) => file.relativePath.startsWith(ignore)));
94
+ results.push(...validFiles);
95
+ } else {
96
+ console.warn(pc.yellow(`Skipping unsupported path: ${relativePath}`));
97
+ }
98
+ }
99
+
100
+ // Deduplicate.
101
+ const filesByDir = new Map();
102
+ for (const file of results) {
103
+ const dir = dirname(file.relativePath);
104
+ const existing = filesByDir.get(dir);
105
+ if (!existing) {
106
+ filesByDir.set(dir, file);
107
+ }
108
+ }
109
+ return Array.from(filesByDir.values());
110
+ }
111
+
112
+ // Run a shell command silently.
113
+ async function runCommand(command, args, options = {}) {
114
+ // Remove clutter by not printing anything; capture output by setting stdio to 'pipe'.
115
+ return await run(command, args, {
116
+ stdio: "pipe",
117
+ env: { NODE_ENV: "test", ...process.env },
118
+ silent: true,
119
+ ...options,
120
+ });
121
+ }
122
+
123
+ // Process a single file.
124
+ async function processFile(file, options) {
125
+ const { fullPath, relativePath } = file;
126
+ const { build, interactive } = options;
127
+ const outputDir = join("test", "e2e", "generated", dirname(relativePath));
128
+ const specCopyPath = join(outputDir, "spec.tsp");
129
+ const logDir = join(projectRoot, "temp", "emit-e2e-logs", dirname(relativePath));
130
+
131
+ let spinner;
132
+ if (interactive) {
133
+ spinner = ora({ text: `Processing: ${relativePath}`, color: "cyan" }).start();
134
+ }
135
+
136
+ try {
137
+ if (await pathExists(outputDir)) {
138
+ if (spinner) spinner.text = `Clearing directory: ${outputDir}`;
139
+ await rm(outputDir, { recursive: true, force: true });
140
+ }
141
+ if (spinner) spinner.text = `Creating directory: ${outputDir}`;
142
+ await mkdir(outputDir, { recursive: true });
143
+
144
+ if (spinner) spinner.text = `Copying spec to: ${specCopyPath}`;
145
+ await copyFile(fullPath, specCopyPath);
146
+
147
+ if (spinner) spinner.text = `Compiling: ${relativePath}`;
148
+ await runCommand("npx", [
149
+ "tsp",
150
+ "compile",
151
+ fullPath,
152
+ "--emit",
153
+ resolve(import.meta.dirname, "../.."),
154
+ "--config",
155
+ tspConfig,
156
+ "--output-dir",
157
+ outputDir,
158
+ ]);
159
+
160
+ if (spinner) spinner.text = `Formatting with Prettier: ${relativePath}`;
161
+ await runCommand("npx", ["prettier", outputDir, "--write"]);
162
+
163
+ if (build) {
164
+ if (spinner) spinner.text = `Building project: ${relativePath}`;
165
+ await runCommand("npm", ["run", "build"], { cwd: outputDir });
166
+ }
167
+
168
+ if (spinner) {
169
+ spinner.succeed(`Finished processing: ${relativePath}`);
170
+ }
171
+ return { status: "succeeded", relativePath };
172
+ } catch (error) {
173
+ if (spinner) {
174
+ spinner.fail(`Failed processing: ${relativePath}`);
175
+ }
176
+ const errorDetails = error.stdout || error.stderr || error.message;
177
+
178
+ // Write error details to a log file.
179
+ await mkdir(logDir, { recursive: true });
180
+ const logFilePath = join(logDir, `${basename(relativePath, ".tsp")}-error.log`);
181
+ await writeFile(logFilePath, errorDetails, "utf8");
182
+
183
+ if (interactive) {
184
+ const { action } = await inquirer.prompt([
185
+ {
186
+ type: "list",
187
+ name: "action",
188
+ message: `Processing failed for ${relativePath}. What would you like to do?`,
189
+ choices: [
190
+ { name: "Retry", value: "retry" },
191
+ { name: "Skip to next file", value: "next" },
192
+ { name: "Abort processing", value: "abort" },
193
+ ],
194
+ },
195
+ ]);
196
+
197
+ if (action === "retry") {
198
+ if (spinner) spinner.start(`Retrying: ${relativePath}`);
199
+ return await processFile(file, options);
200
+ } else if (action === "next") {
201
+ console.log(pc.yellow(`Skipping: ${relativePath}`));
202
+ } else if (action === "abort") {
203
+ console.log(pc.red("Aborting processing."));
204
+ throw new Error("Processing aborted by user");
205
+ }
206
+ }
207
+ return { status: "failed", relativePath, errorDetails };
208
+ }
209
+ }
210
+
211
+ // Process all files.
212
+ async function processFiles(files, options) {
213
+ const { interactive } = options;
214
+ const succeeded = [];
215
+ const failed = [];
216
+
217
+ if (interactive) {
218
+ // Sequential processing so each spinner is visible.
219
+ for (const file of files) {
220
+ try {
221
+ const result = await processFile(file, options);
222
+ if (result.status === "succeeded") {
223
+ succeeded.push(result.relativePath);
224
+ } else {
225
+ failed.push({ relativePath: result.relativePath, errorDetails: result.errorDetails });
226
+ }
227
+ } catch (err) {
228
+ break;
229
+ }
230
+ }
231
+ } else {
232
+ // Global progress spinner.
233
+ const total = files.length;
234
+ let completed = 0;
235
+ const globalSpinner = ora({ text: `Processing 0/${total} files...`, color: "cyan" }).start();
236
+ const limit = pLimit(4);
237
+ const tasks = files.map((file) =>
238
+ limit(() =>
239
+ processFile(file, options).then((result) => {
240
+ completed++;
241
+ globalSpinner.text = `Processing ${completed}/${total} files...`;
242
+ return result;
243
+ }),
244
+ ),
245
+ );
246
+ const results = await Promise.all(tasks);
247
+ globalSpinner.succeed(`Processed ${total} files`);
248
+ for (const result of results) {
249
+ if (result.status === "succeeded") {
250
+ succeeded.push(result.relativePath);
251
+ } else {
252
+ failed.push({ relativePath: result.relativePath, errorDetails: result.errorDetails });
253
+ }
254
+ }
255
+ }
256
+
257
+ console.log(pc.bold(pc.green("\nProcessing Complete:")));
258
+ console.log(pc.green(`Succeeded: ${succeeded.length}`));
259
+ console.log(pc.red(`Failed: ${failed.length}`));
260
+
261
+ if (failed.length > 0) {
262
+ console.log(pc.red("\nFailed Specs:"));
263
+ failed.forEach((f) => {
264
+ console.log(pc.red(` - ${f.relativePath}`));
265
+ });
266
+ console.log(pc.blue(`\nLogs available at: ${logDirRoot}`));
267
+ }
268
+
269
+ // Ensure the log directory exists before writing the report.
270
+ await mkdir(logDirRoot, { recursive: true });
271
+ const report = [
272
+ "Succeeded Files:",
273
+ ...succeeded.map((f) => ` - ${f}`),
274
+ "Failed Files:",
275
+ ...failed.map((f) => ` - ${f.relativePath}\n Error: ${f.errorDetails}`),
276
+ ].join("\n");
277
+ await writeFile(reportFilePath, report, "utf8");
278
+ console.log(pc.blue(`Report written to: ${reportFilePath}`));
279
+ }
280
+ // Main execution function
281
+ async function main() {
282
+ const startTime = process.hrtime.bigint(); // ✅ High precision time tracking
283
+ let exitCode = 0; // ✅ Track success/failure
284
+
285
+ try {
286
+ await clearLogDirectory(); // ✅ Clear logs at the start
287
+
288
+ const ignoreList = await getIgnoreList();
289
+ const paths = argv._.length
290
+ ? await processPaths(argv._, ignoreList)
291
+ : await processPaths(["."], ignoreList);
292
+
293
+ if (paths.length === 0) {
294
+ console.log(pc.yellow("⚠️ No files to process."));
295
+ return;
296
+ }
297
+
298
+ await processFiles(paths, {
299
+ interactive: argv.interactive,
300
+ build: argv.build,
301
+ });
302
+ } catch (error) {
303
+ console.error(pc.red(`❌ Fatal Error: ${error.message}`));
304
+ exitCode = 1; // ✅ Ensure graceful failure handling
305
+ } finally {
306
+ // ✅ Always log execution time before exit
307
+ const endTime = process.hrtime.bigint();
308
+ const duration = Number(endTime - startTime) / 1e9; // Convert nanoseconds to seconds
309
+ console.log(pc.blue(`⏱️ Total execution time: ${duration.toFixed(2)} seconds`));
310
+
311
+ process.exit(exitCode); // ✅ Ensures proper exit handling
312
+ }
313
+ }
314
+
315
+ await main();
@@ -0,0 +1,6 @@
1
+ emitters:
2
+ "@typespec/http-server-js": true
3
+
4
+ options:
5
+ "@typespec/http-server-js":
6
+ emitter-output-dir: "{output-dir}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typespec/http-server-js",
3
- "version": "0.58.0-alpha.12-dev.1",
3
+ "version": "0.58.0-alpha.12-dev.2",
4
4
  "author": "Microsoft Corporation",
5
5
  "description": "TypeSpec HTTP server code generator for JavaScript",
6
6
  "homepage": "https://github.com/microsoft/typespec",
@@ -47,12 +47,23 @@
47
47
  "@types/node": "~22.13.9",
48
48
  "@typespec/compiler": "^0.67.1 || >=0.68.0-dev <0.68.0",
49
49
  "@typespec/http": "^0.67.1 || >=0.68.0-dev <0.68.0",
50
+ "@typespec/http-specs": "^0.1.0-alpha.15 || >=0.1.0-alpha.16-dev <0.1.0-alpha.16",
51
+ "@typespec/internal-build-utils": "^0.67.1 || >=0.68.0-dev <0.68.0",
50
52
  "@typespec/openapi3": "^0.67.1 || >=0.68.0-dev <0.68.0",
53
+ "@typespec/spector": "^0.1.0-alpha.9 || >=0.1.0-alpha.10-dev <0.1.0-alpha.10",
51
54
  "@vitest/coverage-v8": "^3.0.7",
52
55
  "@vitest/ui": "^3.0.7",
56
+ "fs-extra": "^11.2.0",
57
+ "globby": "~14.1.0",
58
+ "inquirer": "^12.2.0",
59
+ "ora": "^8.1.1",
60
+ "p-limit": "^6.2.0",
61
+ "pathe": "^2.0.3",
62
+ "picocolors": "~1.1.1",
53
63
  "tsx": "^4.19.3",
54
64
  "typescript": "~5.8.2",
55
- "vitest": "^3.0.7"
65
+ "vitest": "^3.0.7",
66
+ "yargs": "~17.7.2"
56
67
  },
57
68
  "scripts": {
58
69
  "clean": "rimraf ./dist ./temp",
@@ -66,6 +77,9 @@
66
77
  "test:ci": "vitest run --coverage --reporter=junit --reporter=default",
67
78
  "lint": "eslint . --max-warnings=0",
68
79
  "lint:fix": "eslint . --fix",
69
- "regen-docs": "echo Doc generation disabled for this package."
80
+ "regen-docs": "echo Doc generation disabled for this package.",
81
+ "test:e2e": "npm run emit:e2e && npm run run:e2e",
82
+ "emit:e2e": "node eng/scripts/emit-e2e.js",
83
+ "run:e2e": "vitest run --config ./vitest.config.e2e.js"
70
84
  }
71
85
  }
@@ -0,0 +1,59 @@
1
+ import { createServer, IncomingMessage, Server, ServerResponse } from "node:http";
2
+
3
+ interface BasicRouter {
4
+ /**
5
+ * Dispatches the request to the appropriate service based on the request path.
6
+ *
7
+ * This member function may be used directly as a handler for a Node HTTP server.
8
+ *
9
+ * @param request - The incoming HTTP request.
10
+ * @param response - The outgoing HTTP response.
11
+ */
12
+ dispatch(request: IncomingMessage, response: ServerResponse): void;
13
+ }
14
+
15
+ export function startServer(router: BasicRouter, abortSignal: AbortSignal): Promise<string> {
16
+ return new Promise<string>((resolve, reject) => {
17
+ if (abortSignal.aborted) {
18
+ return reject(new Error("Server start cancelled"));
19
+ }
20
+ const server = createServer((req, res) => router.dispatch(req, res));
21
+ const stop = () => {
22
+ return new Promise<void>((r) => {
23
+ server.close(() => r);
24
+ server.closeAllConnections();
25
+ });
26
+ };
27
+ abortSignal.addEventListener("abort", () => {
28
+ stop().catch(() => {});
29
+ });
30
+ server.listen(function (this: Server) {
31
+ const address = this.address();
32
+ if (!address) {
33
+ reject(new Error("Server address not available"));
34
+ stop().catch(() => {});
35
+ return;
36
+ }
37
+
38
+ resolve(typeof address === "string" ? address : `http://localhost:${address.port}`);
39
+ });
40
+ server.on("error", reject);
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Meant to be used for the `onInternalError` handler in the router
46
+ * in order to log any failed test assertions from within service handlers.
47
+ * Purely informational as the test will fail regardless.
48
+ * @param ctx The HttpContext from the http-server-js router
49
+ */
50
+ function logAssertionErrors(ctx: any, error: Error): void {
51
+ // eslint-disable-next-line no-console
52
+ console.error(error);
53
+ ctx.response.statusCode = 599;
54
+ ctx.response.end();
55
+ }
56
+
57
+ export const testRouterOptions = {
58
+ onInternalError: logAssertionErrors,
59
+ };
@@ -0,0 +1,36 @@
1
+ import { deepStrictEqual } from "node:assert";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { createBasicRouter } from "../../../generated/parameters/basic/src/generated/http/router.js";
4
+ import { startServer, testRouterOptions } from "../../../helpers.js";
5
+ import { runScenario } from "../../../spector.js";
6
+
7
+ describe("Parameters.Basic", () => {
8
+ let serverAbortController: AbortController;
9
+ beforeEach(() => {
10
+ serverAbortController = new AbortController();
11
+ });
12
+ afterEach(() => {
13
+ serverAbortController.abort();
14
+ });
15
+
16
+ it("passes all scenarios", async () => {
17
+ const router = createBasicRouter(
18
+ {
19
+ async simple(ctx, body) {
20
+ deepStrictEqual(body, { name: "foo" });
21
+ return { statusCode: 204 };
22
+ },
23
+ },
24
+ {
25
+ async simple(ctx, name) {
26
+ deepStrictEqual(name, "foo");
27
+ return { statusCode: 204 };
28
+ },
29
+ },
30
+ testRouterOptions,
31
+ );
32
+ const baseUrl = await startServer(router, serverAbortController.signal);
33
+ const { status } = await runScenario("parameters/basic/**/*", baseUrl);
34
+ expect(status).toBe("pass");
35
+ });
36
+ });
@@ -0,0 +1,45 @@
1
+ import { deepStrictEqual } from "node:assert";
2
+ import { afterEach, assert, beforeEach, describe, expect, it } from "vitest";
3
+ import { createBodyOptionalityRouter } from "../../../generated/parameters/body-optionality/src/generated/http/router.js";
4
+ import { startServer, testRouterOptions } from "../../../helpers.js";
5
+ import { runScenario } from "../../../spector.js";
6
+
7
+ describe("Parameters.BodyOptionality", () => {
8
+ let serverAbortController: AbortController;
9
+ beforeEach(() => {
10
+ serverAbortController = new AbortController();
11
+ });
12
+ afterEach(() => {
13
+ serverAbortController.abort();
14
+ });
15
+
16
+ it("passes all scenarios", async () => {
17
+ const router = createBodyOptionalityRouter(
18
+ {
19
+ async requiredExplicit(ctx, body) {
20
+ deepStrictEqual(body, { name: "foo" });
21
+ return { statusCode: 204 };
22
+ },
23
+ async requiredImplicit(ctx, name) {
24
+ deepStrictEqual(name, "foo");
25
+ return { statusCode: 204 };
26
+ },
27
+ },
28
+ {
29
+ async omit(ctx, options) {
30
+ assert.isUndefined(options?.body);
31
+ return { statusCode: 204 };
32
+ },
33
+ async set(ctx, options) {
34
+ assert(options?.body);
35
+ assert.deepStrictEqual(options.body, { name: "foo" });
36
+ return { statusCode: 204 };
37
+ },
38
+ },
39
+ testRouterOptions,
40
+ );
41
+ const baseUrl = await startServer(router, serverAbortController.signal);
42
+ const { status } = await runScenario("parameters/body-optionality/**/*", baseUrl);
43
+ expect(status).toBe("pass");
44
+ });
45
+ });
@@ -0,0 +1,92 @@
1
+ import { deepStrictEqual } from "node:assert";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { createSpreadRouter } from "../../../generated/parameters/spread/src/generated/http/router.js";
4
+ import { startServer, testRouterOptions } from "../../../helpers.js";
5
+ import { runScenario } from "../../../spector.js";
6
+
7
+ describe("Parameters.Spread", () => {
8
+ let serverAbortController: AbortController;
9
+ beforeEach(() => {
10
+ serverAbortController = new AbortController();
11
+ });
12
+ afterEach(() => {
13
+ serverAbortController.abort();
14
+ });
15
+
16
+ it("passes all scenarios", async () => {
17
+ const router = createSpreadRouter(
18
+ {
19
+ async spreadAsRequestBody(ctx, name) {
20
+ deepStrictEqual(name, "foo");
21
+ return { statusCode: 204 };
22
+ },
23
+ async spreadCompositeRequest(ctx, name, testHeader, body) {
24
+ deepStrictEqual(name, "foo");
25
+ deepStrictEqual(testHeader, "bar");
26
+ deepStrictEqual(body, { name: "foo" });
27
+ return { statusCode: 204 };
28
+ },
29
+ async spreadCompositeRequestMix(ctx, name, testHeader, prop) {
30
+ deepStrictEqual(name, "foo");
31
+ deepStrictEqual(testHeader, "bar");
32
+ deepStrictEqual(prop, "foo");
33
+ return { statusCode: 204 };
34
+ },
35
+ async spreadCompositeRequestOnlyWithBody(ctx, body) {
36
+ deepStrictEqual(body, { name: "foo" });
37
+ return { statusCode: 204 };
38
+ },
39
+ async spreadCompositeRequestWithoutBody(ctx, name, testHeader) {
40
+ deepStrictEqual(name, "foo");
41
+ deepStrictEqual(testHeader, "bar");
42
+ return { statusCode: 204 };
43
+ },
44
+ },
45
+ {
46
+ async spreadAsRequestBody(ctx, name) {
47
+ deepStrictEqual(name, "foo");
48
+ return { statusCode: 204 };
49
+ },
50
+ async spreadAsRequestParameter(ctx, id, xMsTestHeader, name) {
51
+ deepStrictEqual(id, "1");
52
+ deepStrictEqual(xMsTestHeader, "bar");
53
+ deepStrictEqual(name, "foo");
54
+ return { statusCode: 204 };
55
+ },
56
+ async spreadParameterWithInnerAlias(ctx, id, name, age, xMsTestHeader) {
57
+ deepStrictEqual(id, "1");
58
+ deepStrictEqual(name, "foo");
59
+ deepStrictEqual(age, 1);
60
+ deepStrictEqual(xMsTestHeader, "bar");
61
+ return { statusCode: 204 };
62
+ },
63
+ async spreadParameterWithInnerModel(ctx, id, name, xMsTestHeader) {
64
+ deepStrictEqual(id, "1");
65
+ deepStrictEqual(name, "foo");
66
+ deepStrictEqual(xMsTestHeader, "bar");
67
+ return { statusCode: 204 };
68
+ },
69
+ async spreadWithMultipleParameters(
70
+ ctx,
71
+ id,
72
+ xMsTestHeader,
73
+ requiredString,
74
+ requiredIntList,
75
+ options,
76
+ ) {
77
+ deepStrictEqual(id, "1");
78
+ deepStrictEqual(xMsTestHeader, "bar");
79
+ deepStrictEqual(requiredString, "foo");
80
+ deepStrictEqual(requiredIntList, [1, 2]);
81
+ deepStrictEqual(options?.optionalInt, 1);
82
+ deepStrictEqual(options?.optionalStringList, ["foo", "bar"]);
83
+ return { statusCode: 204 };
84
+ },
85
+ },
86
+ testRouterOptions,
87
+ );
88
+ const baseUrl = await startServer(router, serverAbortController.signal);
89
+ const { status } = await runScenario("parameters/spread/**/*", baseUrl);
90
+ expect(status).toBe("pass");
91
+ });
92
+ });
@@ -0,0 +1,35 @@
1
+ import { deepStrictEqual } from "node:assert";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { createEmptyRouter } from "../../../../generated/type/model/empty/src/generated/http/router.js";
4
+ import { startServer, testRouterOptions } from "../../../../helpers.js";
5
+ import { runScenario } from "../../../../spector.js";
6
+
7
+ describe("Type.Model.Empty", () => {
8
+ let serverAbortController: AbortController;
9
+ beforeEach(() => {
10
+ serverAbortController = new AbortController();
11
+ });
12
+ afterEach(() => {
13
+ serverAbortController.abort();
14
+ });
15
+
16
+ it("passes all scenarios", async () => {
17
+ const router = createEmptyRouter(
18
+ {
19
+ async getEmpty(ctx) {
20
+ return { body: {} };
21
+ },
22
+ async postRoundTripEmpty(ctx, body) {
23
+ return { body };
24
+ },
25
+ async putEmpty(ctx, input) {
26
+ deepStrictEqual(input, {});
27
+ },
28
+ },
29
+ testRouterOptions,
30
+ );
31
+ const baseUrl = await startServer(router, serverAbortController.signal);
32
+ const { status } = await runScenario("type/model/empty/**/*", baseUrl);
33
+ expect(status).toBe("pass");
34
+ });
35
+ });
@@ -0,0 +1,33 @@
1
+ import { run } from "@typespec/internal-build-utils";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, resolve } from "pathe";
4
+
5
+ // Root of `http-server-js` package so vscode test integration runs from the correct directory
6
+ const CWD = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
7
+
8
+ export async function runScenario(
9
+ scenario: string,
10
+ baseUrl: string,
11
+ ): Promise<{ status: "pass" | "fail" }> {
12
+ try {
13
+ await run(
14
+ "npx",
15
+ [
16
+ "tsp-spector",
17
+ "knock",
18
+ "./node_modules/@typespec/http-specs/specs",
19
+ "--filter",
20
+ scenario,
21
+ "--baseUrl",
22
+ baseUrl,
23
+ ],
24
+ {
25
+ shell: true,
26
+ cwd: CWD,
27
+ },
28
+ );
29
+ return { status: "pass" };
30
+ } catch (e) {
31
+ return { status: "fail" };
32
+ }
33
+ }
@@ -0,0 +1,20 @@
1
+ import { defineConfig, mergeConfig } from "vitest/config";
2
+ import { defaultTypeSpecVitestConfig } from "../../vitest.workspace.js";
3
+
4
+ export default mergeConfig(
5
+ defaultTypeSpecVitestConfig,
6
+ defineConfig({
7
+ test: {
8
+ environment: "node",
9
+ testTimeout: 10_000,
10
+ isolate: false,
11
+ coverage: {
12
+ reporter: ["cobertura", "json", "text"],
13
+ },
14
+ outputFile: {
15
+ junit: "./test-results.xml",
16
+ },
17
+ include: ["test/**/*.e2e.ts"],
18
+ },
19
+ }),
20
+ );
package/vitest.config.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  import { defineConfig, mergeConfig } from "vitest/config";
2
2
  import { defaultTypeSpecVitestConfig } from "../../vitest.workspace.js";
3
3
 
4
- export default mergeConfig(defaultTypeSpecVitestConfig, defineConfig({}));
4
+ export default mergeConfig(
5
+ defaultTypeSpecVitestConfig,
6
+ defineConfig({
7
+ test: {
8
+ include: ["test/**/*.test.ts"],
9
+ },
10
+ }),
11
+ );