@typespec/spector 0.1.0-alpha.9-dev.4 → 0.1.0-dev.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 (74) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +37 -0
  3. package/dist/generated-defs/TypeSpec.Spector.d.ts +4 -4
  4. package/dist/generated-defs/TypeSpec.Spector.d.ts.map +1 -1
  5. package/dist/generated-defs/TypeSpec.Spector.ts-test.js +6 -3
  6. package/dist/generated-defs/TypeSpec.Spector.ts-test.js.map +1 -1
  7. package/dist/src/actions/helper.d.ts +8 -15
  8. package/dist/src/actions/helper.d.ts.map +1 -1
  9. package/dist/src/actions/helper.js +61 -69
  10. package/dist/src/actions/helper.js.map +1 -1
  11. package/dist/src/actions/serve.d.ts.map +1 -1
  12. package/dist/src/actions/serve.js +0 -1
  13. package/dist/src/actions/serve.js.map +1 -1
  14. package/dist/src/actions/server-test.d.ts +3 -5
  15. package/dist/src/actions/server-test.d.ts.map +1 -1
  16. package/dist/src/actions/server-test.js +108 -132
  17. package/dist/src/actions/server-test.js.map +1 -1
  18. package/dist/src/actions/upload-scenario-manifest.d.ts +4 -3
  19. package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -1
  20. package/dist/src/actions/upload-scenario-manifest.js +20 -13
  21. package/dist/src/actions/upload-scenario-manifest.js.map +1 -1
  22. package/dist/src/actions/validate-mock-apis.js +1 -1
  23. package/dist/src/actions/validate-mock-apis.js.map +1 -1
  24. package/dist/src/app/app.d.ts +1 -0
  25. package/dist/src/app/app.d.ts.map +1 -1
  26. package/dist/src/app/app.js +48 -54
  27. package/dist/src/app/app.js.map +1 -1
  28. package/dist/src/app/request-processor.d.ts +2 -2
  29. package/dist/src/app/request-processor.d.ts.map +1 -1
  30. package/dist/src/app/request-processor.js +10 -6
  31. package/dist/src/app/request-processor.js.map +1 -1
  32. package/dist/src/cli/cli.js +20 -21
  33. package/dist/src/cli/cli.js.map +1 -1
  34. package/dist/src/config/config.js +2 -2
  35. package/dist/src/config/config.js.map +1 -1
  36. package/dist/src/coverage/coverage-tracker.d.ts.map +1 -1
  37. package/dist/src/coverage/coverage-tracker.js.map +1 -1
  38. package/dist/src/coverage/scenario-manifest.d.ts +3 -2
  39. package/dist/src/coverage/scenario-manifest.d.ts.map +1 -1
  40. package/dist/src/coverage/scenario-manifest.js +17 -5
  41. package/dist/src/coverage/scenario-manifest.js.map +1 -1
  42. package/dist/src/logger.d.ts +16 -2
  43. package/dist/src/logger.d.ts.map +1 -1
  44. package/dist/src/logger.js +27 -8
  45. package/dist/src/logger.js.map +1 -1
  46. package/dist/src/server/server.d.ts +1 -1
  47. package/dist/src/server/server.d.ts.map +1 -1
  48. package/dist/src/server/server.js +17 -4
  49. package/dist/src/server/server.js.map +1 -1
  50. package/dist/src/utils/misc-utils.d.ts +5 -3
  51. package/dist/src/utils/misc-utils.d.ts.map +1 -1
  52. package/dist/src/utils/misc-utils.js +1 -1
  53. package/dist/src/utils/misc-utils.js.map +1 -1
  54. package/generated-defs/TypeSpec.Spector.ts +4 -3
  55. package/generated-defs/TypeSpec.Spector.ts-test.ts +8 -3
  56. package/lib/main.tsp +1 -1
  57. package/package.json +25 -32
  58. package/src/actions/helper.ts +79 -95
  59. package/src/actions/serve.ts +0 -1
  60. package/src/actions/server-test.ts +132 -156
  61. package/src/actions/upload-scenario-manifest.ts +29 -18
  62. package/src/actions/validate-mock-apis.ts +1 -1
  63. package/src/app/app.ts +71 -72
  64. package/src/app/request-processor.ts +16 -4
  65. package/src/cli/cli.ts +21 -21
  66. package/src/config/config.ts +2 -2
  67. package/src/coverage/coverage-tracker.ts +2 -2
  68. package/src/coverage/scenario-manifest.ts +18 -8
  69. package/src/logger.ts +39 -8
  70. package/src/scenarios-resolver.ts +1 -1
  71. package/src/server/server.ts +20 -6
  72. package/src/utils/misc-utils.ts +6 -4
  73. package/temp/.tsbuildinfo +1 -1
  74. package/vitest.config.ts +11 -0
@@ -7,37 +7,48 @@ import { computeScenarioManifest } from "../coverage/scenario-manifest.js";
7
7
  import { logger } from "../logger.js";
8
8
 
9
9
  export interface UploadScenarioManifestConfig {
10
- scenariosPaths: string[];
10
+ scenariosPath: string;
11
11
  storageAccountName: string;
12
- setNames: string[];
13
12
  containerName: string;
13
+ manifestName: string;
14
+ override?: boolean;
14
15
  }
15
16
 
16
17
  export async function uploadScenarioManifest({
17
- scenariosPaths,
18
+ scenariosPath,
18
19
  storageAccountName,
19
- setNames,
20
20
  containerName,
21
+ manifestName,
22
+ override = false,
21
23
  }: UploadScenarioManifestConfig) {
22
- const manifests = [];
23
- for (let idx = 0; idx < scenariosPaths.length; idx++) {
24
- const path = resolve(process.cwd(), scenariosPaths[idx]);
25
- logger.info(`Computing scenario manifest for ${path}`);
26
- const [manifest, diagnostics] = await computeScenarioManifest(path, setNames[idx]);
27
- if (manifest === undefined || diagnostics.length > 0) {
28
- process.exit(-1);
29
- }
30
- manifests.push(manifest);
24
+ const path = resolve(process.cwd(), scenariosPath);
25
+ logger.info(`Computing scenario manifest for ${path}`);
26
+ const [manifest, diagnostics] = await computeScenarioManifest(path);
27
+ if (manifest === undefined || diagnostics.length > 0) {
28
+ process.exit(-1);
31
29
  }
32
- await writeFile("manifest.json", JSON.stringify(manifests, null, 2));
30
+ await writeFile("manifest.json", JSON.stringify(manifest, null, 2));
33
31
  const client = new SpecCoverageClient(storageAccountName, {
34
32
  credential: new AzureCliCredential(),
35
33
  containerName,
36
34
  });
37
35
  await client.createIfNotExists();
38
- await client.manifest.upload(manifests);
36
+ if (override) {
37
+ await client.manifest.upload(manifestName, manifest);
38
+ logger.info(
39
+ `${pc.green("✓")} Scenario manifest uploaded to ${storageAccountName} storage account.`,
40
+ );
41
+ } else {
42
+ const result = await client.manifest.uploadIfVersionNew(manifestName, manifest);
39
43
 
40
- logger.info(
41
- `${pc.green("✓")} Scenario manifest uploaded to ${storageAccountName} storage account.`,
42
- );
44
+ if (result === "uploaded") {
45
+ logger.info(
46
+ `${pc.green("✓")} Scenario manifest new version uploaded to ${storageAccountName} storage account.`,
47
+ );
48
+ } else {
49
+ logger.info(
50
+ `${pc.white("-")} Existing scenario manifest in ${storageAccountName} storage account is up to date. No upload needed.`,
51
+ );
52
+ }
53
+ }
43
54
  }
@@ -41,7 +41,7 @@ export async function validateMockApis({
41
41
  );
42
42
 
43
43
  if (programDiagnostics.length > 0) {
44
- specCompiler.logDiagnostics(programDiagnostics, { log: logger.error });
44
+ specCompiler.logDiagnostics(programDiagnostics, specCompiler.NodeHost.logSink);
45
45
  diagnostics.reportDiagnostic({
46
46
  message: `Scenario ${name} is invalid.`,
47
47
  });
package/src/app/app.ts CHANGED
@@ -1,4 +1,13 @@
1
- import { MockApiDefinition, MockRequest, RequestExt, ScenarioMockApi } from "@typespec/spec-api";
1
+ import {
2
+ expandDyns,
3
+ MockApiDefinition,
4
+ MockBody,
5
+ MockMultipartBody,
6
+ MockRequest,
7
+ RequestExt,
8
+ ResolverConfig,
9
+ ScenarioMockApi,
10
+ } from "@typespec/spec-api";
2
11
  import { ScenariosMetadata } from "@typespec/spec-coverage-sdk";
3
12
  import { Response, Router } from "express";
4
13
  import { getScenarioMetadata } from "../coverage/common.js";
@@ -19,6 +28,7 @@ export class MockApiApp {
19
28
  private router = Router();
20
29
  private server: MockApiServer;
21
30
  private coverageTracker: CoverageTracker;
31
+ private resolverConfig!: ResolverConfig;
22
32
 
23
33
  constructor(private config: ApiMockAppConfig) {
24
34
  this.server = new MockApiServer({ port: config.port });
@@ -54,79 +64,77 @@ export class MockApiApp {
54
64
  });
55
65
 
56
66
  this.server.use("/", this.router);
57
- this.server.start();
67
+ // Getting the resolved port as setting 0 in the config will have express resolve on of the available ports
68
+ const port = await this.server.start();
69
+ this.resolverConfig = {
70
+ baseUrl: `http://localhost:${port}`,
71
+ };
58
72
  }
59
73
 
60
74
  private registerScenario(name: string, scenario: ScenarioMockApi) {
61
75
  for (const endpoint of scenario.apis) {
62
- if (endpoint.kind !== "MockApiDefinition") {
63
- this.router.route(endpoint.uri)[endpoint.method]((req: RequestExt, res: Response) => {
64
- processRequest(
65
- this.coverageTracker,
66
- name,
67
- endpoint.uri,
68
- req,
69
- res,
70
- endpoint.handler,
71
- ).catch((e) => {
72
- logger.error("Unexpected request error", e);
73
- res.status(500).end();
74
- });
75
- });
76
- } else {
77
- if (!endpoint.handler) {
78
- endpoint.handler = createHandler(endpoint);
79
- }
80
- this.router.route(endpoint.uri)[endpoint.method]((req: RequestExt, res: Response) => {
81
- processRequest(
82
- this.coverageTracker,
83
- name,
84
- endpoint.uri,
85
- req,
86
- res,
87
- endpoint.handler!,
88
- ).catch((e) => {
89
- logger.error("Unexpected request error", e);
90
- res.status(500).end();
91
- });
92
- });
76
+ if (!endpoint.handler) {
77
+ endpoint.handler = createHandler(endpoint, this.resolverConfig);
93
78
  }
79
+ this.router.route(endpoint.uri)[endpoint.method]((req: RequestExt, res: Response) => {
80
+ processRequest(
81
+ this.coverageTracker,
82
+ name,
83
+ endpoint.uri,
84
+ req,
85
+ res,
86
+ endpoint.handler!,
87
+ this.resolverConfig,
88
+ ).catch((e) => {
89
+ logger.error("Unexpected request error", e);
90
+ res.status(500).end();
91
+ });
92
+ });
94
93
  }
95
94
  }
96
95
  }
97
96
 
98
- function isObject(value: any): boolean {
99
- return typeof value === "object" && value !== null && !Array.isArray(value);
97
+ function validateBody(
98
+ req: MockRequest,
99
+ body: MockBody | MockMultipartBody,
100
+ config: ResolverConfig,
101
+ ) {
102
+ if ("kind" in body) {
103
+ // custom handler for now.
104
+ } else {
105
+ if (Buffer.isBuffer(body.rawContent)) {
106
+ req.expect.rawBodyEquals(body.rawContent);
107
+ } else {
108
+ const raw =
109
+ typeof body.rawContent === "string" ? body.rawContent : body.rawContent?.serialize(config);
110
+ switch (body.contentType) {
111
+ case "application/json":
112
+ req.expect.coercedBodyEquals(JSON.parse(raw as any));
113
+ break;
114
+ case "application/xml":
115
+ req.expect.xmlBodyEquals(
116
+ (raw as any).replace(`<?xml version='1.0' encoding='UTF-8'?>`, ""),
117
+ );
118
+ break;
119
+ default:
120
+ req.expect.rawBodyEquals(raw);
121
+ }
122
+ }
123
+ }
100
124
  }
101
125
 
102
- function createHandler(apiDefinition: MockApiDefinition) {
126
+ function createHandler(apiDefinition: MockApiDefinition, config: ResolverConfig) {
103
127
  return (req: MockRequest) => {
128
+ const body = apiDefinition.request?.body;
104
129
  // Validate body if present in the request
105
- if (apiDefinition.request.body) {
106
- if (
107
- apiDefinition.request.headers &&
108
- apiDefinition.request.headers["Content-Type"] === "application/xml"
109
- ) {
110
- req.expect.xmlBodyEquals(
111
- apiDefinition.request.body.rawContent.replace(
112
- `<?xml version='1.0' encoding='UTF-8'?>`,
113
- "",
114
- ),
115
- );
116
- } else {
117
- if (isObject(apiDefinition.request.body)) {
118
- Object.entries(apiDefinition.request.body).forEach(([key, value]) => {
119
- req.expect.deepEqual(req.body[key], value);
120
- });
121
- } else {
122
- req.expect.coercedBodyEquals(apiDefinition.request.body);
123
- }
124
- }
130
+ if (body) {
131
+ validateBody(req, body, config);
125
132
  }
126
133
 
127
134
  // Validate headers if present in the request
128
- if (apiDefinition.request.headers) {
129
- Object.entries(apiDefinition.request.headers).forEach(([key, value]) => {
135
+ if (apiDefinition.request?.headers) {
136
+ const headers = expandDyns(apiDefinition.request.headers, config);
137
+ Object.entries(headers).forEach(([key, value]) => {
130
138
  if (key.toLowerCase() !== "content-type") {
131
139
  if (Array.isArray(value)) {
132
140
  req.expect.deepEqual(req.headers[key], value);
@@ -137,21 +145,12 @@ function createHandler(apiDefinition: MockApiDefinition) {
137
145
  });
138
146
  }
139
147
 
140
- // Validate query params if present in the request
141
- if (apiDefinition.request.params) {
142
- Object.entries(apiDefinition.request.params).forEach(([key, value]) => {
143
- if (!req.query[key]) {
144
- if (Array.isArray(value)) {
145
- req.expect.deepEqual(req.params[key], value);
146
- } else {
147
- req.expect.deepEqual(req.params[key], String(value));
148
- }
148
+ if (apiDefinition.request?.query) {
149
+ Object.entries(apiDefinition.request.query).forEach(([key, value]) => {
150
+ if (Array.isArray(value)) {
151
+ req.expect.deepEqual(req.query[key], value);
149
152
  } else {
150
- if (Array.isArray(value)) {
151
- req.expect.deepEqual(req.query[key], value);
152
- } else {
153
- req.expect.containsQueryParam(key, String(value));
154
- }
153
+ req.expect.containsQueryParam(key, String(value));
155
154
  }
156
155
  });
157
156
  }
@@ -1,8 +1,10 @@
1
1
  import {
2
+ expandDyns,
2
3
  MockRequest,
3
4
  MockRequestHandler,
4
5
  MockResponse,
5
6
  RequestExt,
7
+ ResolverConfig,
6
8
  ValidationError,
7
9
  } from "@typespec/spec-api";
8
10
  import { Response } from "express";
@@ -17,6 +19,7 @@ export async function processRequest(
17
19
  request: RequestExt,
18
20
  response: Response,
19
21
  func: MockRequestHandler,
22
+ resolverConfig: ResolverConfig,
20
23
  ): Promise<void> {
21
24
  const mockRequest = new MockRequest(request);
22
25
  const mockResponse = await callHandler(mockRequest, response, func);
@@ -25,18 +28,27 @@ export async function processRequest(
25
28
  }
26
29
 
27
30
  await coverageTracker.trackEndpointResponse(scenarioName, scenarioUri, mockResponse);
28
- processResponse(response, mockResponse);
31
+ processResponse(response, mockResponse, resolverConfig);
29
32
  }
30
33
 
31
- const processResponse = (response: Response, mockResponse: MockResponse) => {
34
+ const processResponse = (
35
+ response: Response,
36
+ mockResponse: MockResponse,
37
+ resolverConfig: ResolverConfig,
38
+ ) => {
32
39
  response.status(mockResponse.status);
33
40
 
34
41
  if (mockResponse.headers) {
35
- response.set(mockResponse.headers);
42
+ response.set(expandDyns(mockResponse.headers, resolverConfig));
36
43
  }
37
44
 
38
45
  if (mockResponse.body) {
39
- response.contentType(mockResponse.body.contentType).send(mockResponse.body.rawContent);
46
+ const raw =
47
+ typeof mockResponse.body.rawContent === "string" ||
48
+ Buffer.isBuffer(mockResponse.body.rawContent)
49
+ ? mockResponse.body.rawContent
50
+ : mockResponse.body.rawContent?.serialize(resolverConfig);
51
+ response.contentType(mockResponse.body.contentType).send(raw);
40
52
  }
41
53
 
42
54
  response.end();
package/src/cli/cli.ts CHANGED
@@ -167,7 +167,7 @@ async function main() {
167
167
  },
168
168
  )
169
169
  .command(
170
- "server-test <scenariosPaths..>",
170
+ "knock <scenariosPaths..>",
171
171
  "Executes the test cases against the service",
172
172
  (cmd) => {
173
173
  return cmd
@@ -181,23 +181,18 @@ async function main() {
181
181
  description: "Path to the server",
182
182
  type: "string",
183
183
  })
184
- .option("runSingleScenario", {
185
- description: "Single Scenario Case to run",
184
+ .option("filter", {
185
+ description: "Glob filter of scenario to run",
186
186
  type: "string",
187
187
  })
188
- .option("runScenariosFromFile", {
189
- description: "File that has the Scenarios to run",
190
- type: "string",
191
- })
192
- .demandOption("scenariosPaths", "serverBasePath");
188
+ .demandOption("scenariosPaths");
193
189
  },
194
190
  async (args) => {
195
191
  for (const scenariosPath of args.scenariosPaths) {
196
192
  logger.info(`Executing server tests for scenarios at ${scenariosPath}`);
197
193
  await serverTest(scenariosPath, {
198
194
  baseUrl: args.baseUrl,
199
- runSingleScenario: args.runSingleScenario,
200
- runScenariosFromFile: args.runScenariosFromFile,
195
+ filter: args.filter,
201
196
  });
202
197
  }
203
198
  },
@@ -278,20 +273,13 @@ async function main() {
278
273
  },
279
274
  )
280
275
  .command(
281
- "upload-manifest <scenariosPaths..>",
276
+ "upload-manifest <scenariosPath>",
282
277
  "Upload the scenario manifest. DO NOT CALL in generator.",
283
278
  (cmd) => {
284
279
  return cmd
285
- .positional("scenariosPaths", {
280
+ .positional("scenariosPath", {
286
281
  description: "Path to the scenarios and mock apis",
287
282
  type: "string",
288
- array: true,
289
- demandOption: true,
290
- })
291
- .option("setName", {
292
- type: "string",
293
- description: "Set used to generate the manifest.",
294
- array: true,
295
283
  demandOption: true,
296
284
  })
297
285
  .option("storageAccountName", {
@@ -303,14 +291,26 @@ async function main() {
303
291
  description: "Name of the Container",
304
292
  demandOption: true,
305
293
  })
294
+ .option("manifestName", {
295
+ type: "string",
296
+ description:
297
+ "Name of the manifest(will be located at manifests/<manifestName>.json in the container).",
298
+ demandOption: true,
299
+ })
300
+ .option("override", {
301
+ type: "boolean",
302
+ description: "Override existing manifest with the same version.",
303
+ default: false,
304
+ })
306
305
  .demandOption("storageAccountName");
307
306
  },
308
307
  async (args) => {
309
308
  await uploadScenarioManifest({
310
- scenariosPaths: args.scenariosPaths,
309
+ scenariosPath: args.scenariosPath,
311
310
  storageAccountName: args.storageAccountName,
312
- setNames: args.setName,
313
311
  containerName: args.containerName,
312
+ manifestName: args.manifestName,
313
+ override: args.override,
314
314
  });
315
315
  },
316
316
  )
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "fs/promises";
2
- import yaml from "js-yaml";
2
+ import yaml from "yaml";
3
3
  import { Diagnostic } from "../utils/diagnostic-reporter.js";
4
4
  import { SpecConfigJsonSchema } from "./config-schema.js";
5
5
  import { SchemaValidator } from "./schema-validator.js";
@@ -9,7 +9,7 @@ const validator = new SchemaValidator(SpecConfigJsonSchema);
9
9
 
10
10
  export async function loadSpecConfig(path: string): Promise<[SpecConfig, Diagnostic[]]> {
11
11
  const content = await readFile(path);
12
- const config: any = yaml.load(content.toString(), { filename: path });
12
+ const config: any = yaml.parse(content.toString());
13
13
  const diagnostics = validator.validate(config, path);
14
14
  return [config, diagnostics];
15
15
  }
@@ -1,4 +1,4 @@
1
- import { Fail, KeyedMockResponse, MockResponse, PassByKeyScenario, PassByServiceKeyScenario, ScenarioMockApi } from "@typespec/spec-api";
1
+ import { Fail, KeyedMockResponse, MockResponse, PassByKeyScenario, ScenarioMockApi } from "@typespec/spec-api";
2
2
  import { logger } from "../logger.js";
3
3
  import { CoverageReport, ScenariosMetadata, ScenarioStatus } from "@typespec/spec-coverage-sdk";
4
4
  import { writeFileSync } from "fs";
@@ -106,7 +106,7 @@ export class CoverageTracker {
106
106
  return "pass";
107
107
  }
108
108
 
109
- function checkByKeys(scenario: PassByKeyScenario | PassByServiceKeyScenario) {
109
+ function checkByKeys(scenario: PassByKeyScenario) {
110
110
  for (const endpoint of scenario.apis) {
111
111
  const hits = scenarioHits?.get(endpoint.uri);
112
112
  if (hits === undefined) {
@@ -1,14 +1,13 @@
1
1
  import { loadScenarios } from "../scenarios-resolver.js";
2
2
  import { Diagnostic } from "../utils/diagnostic-reporter.js";
3
- import { getCommit, getPackageJson } from "../utils/misc-utils.js";
3
+ import { getCommit, getPackageJson, type SpectorPackageJson } from "../utils/misc-utils.js";
4
4
  import { ScenarioLocation, ScenarioManifest } from "@typespec/spec-coverage-sdk";
5
- import { getSourceLocation, normalizePath } from "@typespec/compiler";
5
+ import { getSourceLocation, normalizePath, PackageJson } from "@typespec/compiler";
6
6
  import { relative } from "path";
7
7
  import type { Scenario } from "../lib/decorators.js";
8
8
 
9
9
  export async function computeScenarioManifest(
10
10
  scenariosPath: string,
11
- setName: string
12
11
  ): Promise<[ScenarioManifest | undefined, readonly Diagnostic[]]> {
13
12
  const [scenarios, diagnostics] = await loadScenarios(scenariosPath);
14
13
  if (diagnostics.length > 0) {
@@ -17,19 +16,31 @@ export async function computeScenarioManifest(
17
16
 
18
17
  const commit = getCommit(scenariosPath);
19
18
  const pkg = await getPackageJson(scenariosPath);
20
- return [createScenarioManifest(scenariosPath, pkg?.version ?? "?", commit, scenarios, setName), []];
19
+ return [createScenarioManifest(scenariosPath, pkg, commit, scenarios), []];
21
20
  }
22
21
 
22
+ function getRepo(pkg: PackageJson): string | undefined {
23
+ const repository = pkg.repository;
24
+ const gitUrl = typeof repository === "string" ? repository : repository?.url;
25
+ if (!gitUrl) {
26
+ return undefined;
27
+ }
28
+ // Parse git+https://github.com/org/repo.git to https://github.com/org/repo
29
+ return gitUrl.replace(/^git\+/, "").replace(/\.git$/, "");
30
+ }
23
31
  export function createScenarioManifest(
24
32
  scenariosPath: string,
25
- version: string,
33
+ pkg: SpectorPackageJson | undefined,
26
34
  commit: string,
27
35
  scenarios: Scenario[],
28
- setName: string
29
36
  ): ScenarioManifest {
30
37
  const sortedScenarios = [...scenarios].sort((a, b) => a.name.localeCompare(b.name));
31
38
  return {
32
- version,
39
+ version: pkg?.version ?? "?",
40
+ repo: pkg && getRepo(pkg),
41
+ sourceUrl: pkg?.spector?.sourceUrl,
42
+ packageName: pkg?.name,
43
+ displayName: pkg && ("displayName" in pkg ? pkg.displayName as string : undefined),
33
44
  commit,
34
45
  scenarios: sortedScenarios.map(({ name, scenarioDoc, target }) => {
35
46
  const tspLocation = getSourceLocation(target);
@@ -40,6 +51,5 @@ export function createScenarioManifest(
40
51
  };
41
52
  return { name, scenarioDoc, location };
42
53
  }),
43
- setName
44
54
  };
45
55
  }
package/src/logger.ts CHANGED
@@ -1,10 +1,41 @@
1
- import winston from "winston";
1
+ /* eslint-disable no-console */
2
+ import pc from "picocolors";
2
3
 
3
- export const logger = winston.createLogger({
4
+ const levels = {
5
+ debug: 10,
6
+ info: 20,
7
+ warn: 30,
8
+ error: 30,
9
+ };
10
+
11
+ type Level = keyof typeof levels;
12
+ const levelDisplay: Record<Level, string> = {
13
+ debug: pc.blue("debug"),
14
+ info: pc.green("info"),
15
+ warn: pc.yellow("warn"),
16
+ error: pc.red("error"),
17
+ };
18
+
19
+ interface Logger {
20
+ level: Level;
21
+ debug: (message: string, ...data: unknown[]) => void;
22
+ info: (message: string, ...data: unknown[]) => void;
23
+ warn: (message: string, ...data: unknown[]) => void;
24
+ error: (message: string, ...data: unknown[]) => void;
25
+ }
26
+
27
+ export const logger: Logger = {
4
28
  level: "info",
5
- transports: [
6
- new winston.transports.Console({
7
- format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
8
- }),
9
- ],
10
- });
29
+ debug: log("debug"),
30
+ info: log("info"),
31
+ warn: log("warn"),
32
+ error: log("error"),
33
+ };
34
+
35
+ function log(level: Level) {
36
+ return (message: string, ...data: unknown[]) => {
37
+ if (levels[level] >= levels[logger.level]) {
38
+ console.log(levelDisplay[level], message, ...data);
39
+ }
40
+ };
41
+ }
@@ -1,4 +1,4 @@
1
- import { Operation } from "@typespec/compiler";
1
+ import type { Operation } from "@typespec/compiler";
2
2
  import { isSharedRoute } from "@typespec/http";
3
3
  import { ScenarioMockApi } from "@typespec/spec-api";
4
4
  import { dirname, join, relative, resolve } from "path";
@@ -75,25 +75,39 @@ export class MockApiServer {
75
75
  verify: rawBinaryBodySaver,
76
76
  }),
77
77
  );
78
- this.app.use(multer().any());
78
+ this.app.use(multer().any() as any);
79
79
  }
80
80
 
81
81
  public use(route: string, ...handlers: RequestHandler[]): void {
82
82
  this.app.use(route, ...handlers);
83
83
  }
84
84
 
85
- public start(): void {
85
+ public start(): Promise<number> {
86
86
  this.app.use(errorHandler);
87
87
 
88
- const server = this.app.listen(this.config.port, () => {
89
- logger.info(`Started server on ${getAddress(server)}`);
88
+ return new Promise((resolve, reject) => {
89
+ const server = this.app.listen(this.config.port, () => {
90
+ const resolvedPort = getPort(server);
91
+ if (!resolvedPort) {
92
+ logger.error("Failed to resolve port");
93
+ reject(new Error("Failed to resolve port"));
94
+ return;
95
+ }
96
+ logger.info(`Started server on ${resolvedPort}`);
97
+ resolve(resolvedPort);
98
+ });
99
+
100
+ server.on("error", (err) => {
101
+ logger.error("Error starting server", err);
102
+ reject(err);
103
+ });
90
104
  });
91
105
  }
92
106
  }
93
107
 
94
108
  export type ServerRequestHandler = (request: RequestExt, response: Response) => void;
95
109
 
96
- const getAddress = (server: Server): string => {
110
+ const getPort = (server: Server): number | undefined | null => {
97
111
  const address = server?.address();
98
- return typeof address === "string" ? "pipe " + address : "port " + address?.port;
112
+ return typeof address === "string" ? null : address?.port;
99
113
  };
@@ -1,3 +1,4 @@
1
+ import type { PackageJson } from "@typespec/compiler";
1
2
  import { execSync } from "child_process";
2
3
  import { readFile, stat } from "fs/promises";
3
4
  import { dirname, join } from "path";
@@ -10,7 +11,7 @@ export async function ensureScenariosPathExists(scenariosPath: string) {
10
11
  throw new Error(`Scenarios path ${scenariosPath} is not a directory.`);
11
12
  }
12
13
  } catch (e) {
13
- throw new Error(`Scenarios path ${scenariosPath} doesn't exists.`);
14
+ throw new Error(`Scenarios path ${scenariosPath} doesn't exist.`, { cause: e });
14
15
  }
15
16
  }
16
17
 
@@ -18,9 +19,10 @@ export function getCommit(path: string): string {
18
19
  return execSync("git rev-parse HEAD", { cwd: path }).toString().trim();
19
20
  }
20
21
 
21
- export interface PackageJson {
22
- name: string;
23
- version: string;
22
+ export interface SpectorPackageJson extends PackageJson {
23
+ spector?: {
24
+ sourceUrl?: string;
25
+ };
24
26
  }
25
27
 
26
28
  export async function getPackageJson(path: string): Promise<PackageJson | undefined> {