@typespec/spector 0.1.0-alpha.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 (254) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/LICENSE +21 -0
  3. package/cmd/cli.mjs +3 -0
  4. package/dist/generated-defs/TypeSpec.Spector.d.ts +22 -0
  5. package/dist/generated-defs/TypeSpec.Spector.d.ts.map +1 -0
  6. package/dist/generated-defs/TypeSpec.Spector.js +2 -0
  7. package/dist/generated-defs/TypeSpec.Spector.js.map +1 -0
  8. package/dist/generated-defs/TypeSpec.Spector.ts-test.d.ts +2 -0
  9. package/dist/generated-defs/TypeSpec.Spector.ts-test.d.ts.map +1 -0
  10. package/dist/generated-defs/TypeSpec.Spector.ts-test.js +5 -0
  11. package/dist/generated-defs/TypeSpec.Spector.ts-test.js.map +1 -0
  12. package/dist/src/actions/check-coverage.d.ts +11 -0
  13. package/dist/src/actions/check-coverage.d.ts.map +1 -0
  14. package/dist/src/actions/check-coverage.js +77 -0
  15. package/dist/src/actions/check-coverage.js.map +1 -0
  16. package/dist/src/actions/generate-scenario-summary.d.ts +9 -0
  17. package/dist/src/actions/generate-scenario-summary.d.ts.map +1 -0
  18. package/dist/src/actions/generate-scenario-summary.js +49 -0
  19. package/dist/src/actions/generate-scenario-summary.js.map +1 -0
  20. package/dist/src/actions/helper.d.ts +21 -0
  21. package/dist/src/actions/helper.d.ts.map +1 -0
  22. package/dist/src/actions/helper.js +81 -0
  23. package/dist/src/actions/helper.js.map +1 -0
  24. package/dist/src/actions/index.d.ts +2 -0
  25. package/dist/src/actions/index.d.ts.map +1 -0
  26. package/dist/src/actions/index.js +2 -0
  27. package/dist/src/actions/index.js.map +1 -0
  28. package/dist/src/actions/serve.d.ts +12 -0
  29. package/dist/src/actions/serve.d.ts.map +1 -0
  30. package/dist/src/actions/serve.js +73 -0
  31. package/dist/src/actions/serve.js.map +1 -0
  32. package/dist/src/actions/server-test.d.ts +7 -0
  33. package/dist/src/actions/server-test.d.ts.map +1 -0
  34. package/dist/src/actions/server-test.js +165 -0
  35. package/dist/src/actions/server-test.js.map +1 -0
  36. package/dist/src/actions/upload-coverage-report.d.ts +10 -0
  37. package/dist/src/actions/upload-coverage-report.d.ts.map +1 -0
  38. package/dist/src/actions/upload-coverage-report.js +19 -0
  39. package/dist/src/actions/upload-coverage-report.js.map +1 -0
  40. package/dist/src/actions/upload-scenario-manifest.d.ts +6 -0
  41. package/dist/src/actions/upload-scenario-manifest.d.ts.map +1 -0
  42. package/dist/src/actions/upload-scenario-manifest.js +18 -0
  43. package/dist/src/actions/upload-scenario-manifest.js.map +1 -0
  44. package/dist/src/actions/validate-mock-apis.d.ts +7 -0
  45. package/dist/src/actions/validate-mock-apis.d.ts.map +1 -0
  46. package/dist/src/actions/validate-mock-apis.js +71 -0
  47. package/dist/src/actions/validate-mock-apis.js.map +1 -0
  48. package/dist/src/actions/validate-scenarios.d.ts +7 -0
  49. package/dist/src/actions/validate-scenarios.d.ts.map +1 -0
  50. package/dist/src/actions/validate-scenarios.js +25 -0
  51. package/dist/src/actions/validate-scenarios.js.map +1 -0
  52. package/dist/src/app/app.d.ts +17 -0
  53. package/dist/src/app/app.d.ts.map +1 -0
  54. package/dist/src/app/app.js +134 -0
  55. package/dist/src/app/app.js.map +1 -0
  56. package/dist/src/app/config.d.ts +15 -0
  57. package/dist/src/app/config.d.ts.map +1 -0
  58. package/dist/src/app/config.js +2 -0
  59. package/dist/src/app/config.js.map +1 -0
  60. package/dist/src/app/index.d.ts +3 -0
  61. package/dist/src/app/index.d.ts.map +1 -0
  62. package/dist/src/app/index.js +3 -0
  63. package/dist/src/app/index.js.map +1 -0
  64. package/dist/src/app/request-processor.d.ts +5 -0
  65. package/dist/src/app/request-processor.d.ts.map +1 -0
  66. package/dist/src/app/request-processor.js +44 -0
  67. package/dist/src/app/request-processor.js.map +1 -0
  68. package/dist/src/cli/cli.d.ts +3 -0
  69. package/dist/src/cli/cli.d.ts.map +1 -0
  70. package/dist/src/cli/cli.js +316 -0
  71. package/dist/src/cli/cli.js.map +1 -0
  72. package/dist/src/config/config-schema.d.ts +4 -0
  73. package/dist/src/config/config-schema.d.ts.map +1 -0
  74. package/dist/src/config/config-schema.js +14 -0
  75. package/dist/src/config/config-schema.js.map +1 -0
  76. package/dist/src/config/config.d.ts +4 -0
  77. package/dist/src/config/config.d.ts.map +1 -0
  78. package/dist/src/config/config.js +12 -0
  79. package/dist/src/config/config.js.map +1 -0
  80. package/dist/src/config/schema-validator.d.ts +18 -0
  81. package/dist/src/config/schema-validator.d.ts.map +1 -0
  82. package/dist/src/config/schema-validator.js +43 -0
  83. package/dist/src/config/schema-validator.js.map +1 -0
  84. package/dist/src/config/types.d.ts +4 -0
  85. package/dist/src/config/types.d.ts.map +1 -0
  86. package/dist/src/config/types.js +2 -0
  87. package/dist/src/config/types.js.map +1 -0
  88. package/dist/src/constants.d.ts +4 -0
  89. package/dist/src/constants.d.ts.map +1 -0
  90. package/dist/src/constants.js +4 -0
  91. package/dist/src/constants.js.map +1 -0
  92. package/dist/src/coverage/common.d.ts +3 -0
  93. package/dist/src/coverage/common.d.ts.map +1 -0
  94. package/dist/src/coverage/common.js +9 -0
  95. package/dist/src/coverage/common.js.map +1 -0
  96. package/dist/src/coverage/coverage-report.d.ts +3 -0
  97. package/dist/src/coverage/coverage-report.d.ts.map +1 -0
  98. package/dist/src/coverage/coverage-report.js +9 -0
  99. package/dist/src/coverage/coverage-report.js.map +1 -0
  100. package/dist/src/coverage/coverage-tracker.d.ts +17 -0
  101. package/dist/src/coverage/coverage-tracker.d.ts.map +1 -0
  102. package/dist/src/coverage/coverage-tracker.js +125 -0
  103. package/dist/src/coverage/coverage-tracker.js.map +1 -0
  104. package/dist/src/coverage/index.d.ts +2 -0
  105. package/dist/src/coverage/index.d.ts.map +1 -0
  106. package/dist/src/coverage/index.js +2 -0
  107. package/dist/src/coverage/index.js.map +1 -0
  108. package/dist/src/coverage/scenario-manifest.d.ts +6 -0
  109. package/dist/src/coverage/scenario-manifest.d.ts.map +1 -0
  110. package/dist/src/coverage/scenario-manifest.js +32 -0
  111. package/dist/src/coverage/scenario-manifest.js.map +1 -0
  112. package/dist/src/index.d.ts +2 -0
  113. package/dist/src/index.d.ts.map +1 -0
  114. package/dist/src/index.js +2 -0
  115. package/dist/src/index.js.map +1 -0
  116. package/dist/src/lib/decorators.d.ts +24 -0
  117. package/dist/src/lib/decorators.d.ts.map +1 -0
  118. package/dist/src/lib/decorators.js +158 -0
  119. package/dist/src/lib/decorators.js.map +1 -0
  120. package/dist/src/lib/index.d.ts +4 -0
  121. package/dist/src/lib/index.d.ts.map +1 -0
  122. package/dist/src/lib/index.js +6 -0
  123. package/dist/src/lib/index.js.map +1 -0
  124. package/dist/src/lib/lib.d.ts +33 -0
  125. package/dist/src/lib/lib.d.ts.map +1 -0
  126. package/dist/src/lib/lib.js +31 -0
  127. package/dist/src/lib/lib.js.map +1 -0
  128. package/dist/src/lib/tsp-index.d.ts +2 -0
  129. package/dist/src/lib/tsp-index.d.ts.map +1 -0
  130. package/dist/src/lib/tsp-index.js +11 -0
  131. package/dist/src/lib/tsp-index.js.map +1 -0
  132. package/dist/src/lib/validate.d.ts +3 -0
  133. package/dist/src/lib/validate.d.ts.map +1 -0
  134. package/dist/src/lib/validate.js +41 -0
  135. package/dist/src/lib/validate.js.map +1 -0
  136. package/dist/src/logger.d.ts +3 -0
  137. package/dist/src/logger.d.ts.map +1 -0
  138. package/dist/src/logger.js +10 -0
  139. package/dist/src/logger.js.map +1 -0
  140. package/dist/src/routes/admin.d.ts +3 -0
  141. package/dist/src/routes/admin.d.ts.map +1 -0
  142. package/dist/src/routes/admin.js +13 -0
  143. package/dist/src/routes/admin.js.map +1 -0
  144. package/dist/src/routes/index.d.ts +3 -0
  145. package/dist/src/routes/index.d.ts.map +1 -0
  146. package/dist/src/routes/index.js +6 -0
  147. package/dist/src/routes/index.js.map +1 -0
  148. package/dist/src/scenarios-resolver.d.ts +17 -0
  149. package/dist/src/scenarios-resolver.d.ts.map +1 -0
  150. package/dist/src/scenarios-resolver.js +163 -0
  151. package/dist/src/scenarios-resolver.js.map +1 -0
  152. package/dist/src/server/index.d.ts +2 -0
  153. package/dist/src/server/index.d.ts.map +1 -0
  154. package/dist/src/server/index.js +2 -0
  155. package/dist/src/server/index.js.map +1 -0
  156. package/dist/src/server/server.d.ts +14 -0
  157. package/dist/src/server/server.d.ts.map +1 -0
  158. package/dist/src/server/server.js +68 -0
  159. package/dist/src/server/server.js.map +1 -0
  160. package/dist/src/spec-utils/import-spec.d.ts +6 -0
  161. package/dist/src/spec-utils/import-spec.d.ts.map +1 -0
  162. package/dist/src/spec-utils/import-spec.js +39 -0
  163. package/dist/src/spec-utils/import-spec.js.map +1 -0
  164. package/dist/src/spec-utils/index.d.ts +2 -0
  165. package/dist/src/spec-utils/index.d.ts.map +1 -0
  166. package/dist/src/spec-utils/index.js +2 -0
  167. package/dist/src/spec-utils/index.js.map +1 -0
  168. package/dist/src/utils/body-utils.d.ts +8 -0
  169. package/dist/src/utils/body-utils.d.ts.map +1 -0
  170. package/dist/src/utils/body-utils.js +8 -0
  171. package/dist/src/utils/body-utils.js.map +1 -0
  172. package/dist/src/utils/diagnostic-reporter.d.ts +13 -0
  173. package/dist/src/utils/diagnostic-reporter.d.ts.map +1 -0
  174. package/dist/src/utils/diagnostic-reporter.js +35 -0
  175. package/dist/src/utils/diagnostic-reporter.js.map +1 -0
  176. package/dist/src/utils/exec.d.ts +9 -0
  177. package/dist/src/utils/exec.d.ts.map +1 -0
  178. package/dist/src/utils/exec.js +30 -0
  179. package/dist/src/utils/exec.js.map +1 -0
  180. package/dist/src/utils/file-utils.d.ts +7 -0
  181. package/dist/src/utils/file-utils.d.ts.map +1 -0
  182. package/dist/src/utils/file-utils.js +13 -0
  183. package/dist/src/utils/file-utils.js.map +1 -0
  184. package/dist/src/utils/index.d.ts +6 -0
  185. package/dist/src/utils/index.d.ts.map +1 -0
  186. package/dist/src/utils/index.js +6 -0
  187. package/dist/src/utils/index.js.map +1 -0
  188. package/dist/src/utils/misc-utils.d.ts +8 -0
  189. package/dist/src/utils/misc-utils.d.ts.map +1 -0
  190. package/dist/src/utils/misc-utils.js +47 -0
  191. package/dist/src/utils/misc-utils.js.map +1 -0
  192. package/dist/src/utils/path-utils.d.ts +2 -0
  193. package/dist/src/utils/path-utils.d.ts.map +1 -0
  194. package/dist/src/utils/path-utils.js +4 -0
  195. package/dist/src/utils/path-utils.js.map +1 -0
  196. package/dist/src/utils/request-utils.d.ts +3 -0
  197. package/dist/src/utils/request-utils.d.ts.map +1 -0
  198. package/dist/src/utils/request-utils.js +2 -0
  199. package/dist/src/utils/request-utils.js.map +1 -0
  200. package/generated-defs/TypeSpec.Spector.ts +46 -0
  201. package/generated-defs/TypeSpec.Spector.ts-test.ts +5 -0
  202. package/lib/main.tsp +37 -0
  203. package/package.json +79 -0
  204. package/src/actions/check-coverage.ts +98 -0
  205. package/src/actions/generate-scenario-summary.ts +63 -0
  206. package/src/actions/helper.ts +116 -0
  207. package/src/actions/index.ts +1 -0
  208. package/src/actions/serve.ts +88 -0
  209. package/src/actions/server-test.ts +198 -0
  210. package/src/actions/upload-coverage-report.ts +41 -0
  211. package/src/actions/upload-scenario-manifest.ts +30 -0
  212. package/src/actions/validate-mock-apis.ts +91 -0
  213. package/src/actions/validate-scenarios.ts +32 -0
  214. package/src/app/app.ts +166 -0
  215. package/src/app/config.ts +16 -0
  216. package/src/app/index.ts +2 -0
  217. package/src/app/request-processor.ts +71 -0
  218. package/src/cli/cli.ts +368 -0
  219. package/src/config/config-schema.ts +16 -0
  220. package/src/config/config.ts +15 -0
  221. package/src/config/schema-validator.ts +57 -0
  222. package/src/config/types.ts +3 -0
  223. package/src/constants.ts +3 -0
  224. package/src/coverage/common.ts +10 -0
  225. package/src/coverage/coverage-report.ts +13 -0
  226. package/src/coverage/coverage-tracker.ts +141 -0
  227. package/src/coverage/index.ts +1 -0
  228. package/src/coverage/scenario-manifest.ts +43 -0
  229. package/src/index.ts +1 -0
  230. package/src/lib/decorators.ts +225 -0
  231. package/src/lib/index.ts +18 -0
  232. package/src/lib/lib.ts +33 -0
  233. package/src/lib/tsp-index.ts +12 -0
  234. package/src/lib/validate.ts +55 -0
  235. package/src/logger.ts +10 -0
  236. package/src/routes/admin.ts +15 -0
  237. package/src/routes/index.ts +7 -0
  238. package/src/scenarios-resolver.ts +209 -0
  239. package/src/server/index.ts +1 -0
  240. package/src/server/server.ts +99 -0
  241. package/src/spec-utils/import-spec.ts +49 -0
  242. package/src/spec-utils/index.ts +1 -0
  243. package/src/utils/body-utils.test.ts +18 -0
  244. package/src/utils/body-utils.ts +8 -0
  245. package/src/utils/diagnostic-reporter.ts +49 -0
  246. package/src/utils/exec.ts +36 -0
  247. package/src/utils/file-utils.ts +14 -0
  248. package/src/utils/index.ts +5 -0
  249. package/src/utils/misc-utils.ts +54 -0
  250. package/src/utils/path-utils.ts +3 -0
  251. package/src/utils/request-utils.ts +4 -0
  252. package/temp/.tsbuildinfo +1 -0
  253. package/tsconfig.build.json +13 -0
  254. package/tsconfig.json +4 -0
@@ -0,0 +1,8 @@
1
+ export declare function ensureScenariosPathExists(scenariosPath: string): Promise<void>;
2
+ export declare function getCommit(path: string): string;
3
+ export interface PackageJson {
4
+ name: string;
5
+ version: string;
6
+ }
7
+ export declare function getPackageJson(path: string): Promise<PackageJson | undefined>;
8
+ //# sourceMappingURL=misc-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"misc-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/misc-utils.ts"],"names":[],"mappings":"AAKA,wBAAsB,yBAAyB,CAAC,aAAa,EAAE,MAAM,iBASpE;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CASnF"}
@@ -0,0 +1,47 @@
1
+ import { execSync } from "child_process";
2
+ import { readFile, stat } from "fs/promises";
3
+ import { dirname, join } from "path";
4
+ import { logger } from "../logger.js";
5
+ export async function ensureScenariosPathExists(scenariosPath) {
6
+ try {
7
+ const stats = await stat(scenariosPath);
8
+ if (!stats.isDirectory()) {
9
+ throw new Error(`Scenarios path ${scenariosPath} is not a directory.`);
10
+ }
11
+ }
12
+ catch (e) {
13
+ throw new Error(`Scenarios path ${scenariosPath} doesn't exists.`);
14
+ }
15
+ }
16
+ export function getCommit(path) {
17
+ return execSync("git rev-parse HEAD", { cwd: path }).toString().trim();
18
+ }
19
+ export async function getPackageJson(path) {
20
+ logger.debug(`Loading package json for path "${path}"`);
21
+ const projectRoot = await findProjectRoot(path);
22
+ if (projectRoot === undefined) {
23
+ return undefined;
24
+ }
25
+ logger.debug(`Found project root "${projectRoot}"`);
26
+ const packageJsonPath = join(projectRoot, "package.json");
27
+ return JSON.parse((await readFile(packageJsonPath, "utf-8")).toString());
28
+ }
29
+ async function findProjectRoot(path) {
30
+ let current = path;
31
+ while (true) {
32
+ const pkgPath = join(current, "package.json");
33
+ try {
34
+ const stats = await stat(pkgPath);
35
+ if (stats?.isFile()) {
36
+ return current;
37
+ }
38
+ }
39
+ catch { }
40
+ const parent = dirname(current);
41
+ if (parent === current) {
42
+ return undefined;
43
+ }
44
+ current = parent;
45
+ }
46
+ }
47
+ //# sourceMappingURL=misc-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"misc-utils.js","sourceRoot":"","sources":["../../../src/utils/misc-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,aAAqB;IACnE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,kBAAkB,aAAa,sBAAsB,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,kBAAkB,aAAa,kBAAkB,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,QAAQ,CAAC,oBAAoB,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;AACzE,CAAC;AAOD,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY;IAC/C,MAAM,CAAC,KAAK,CAAC,kCAAkC,IAAI,GAAG,CAAC,CAAC;IACxD,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,uBAAuB,WAAW,GAAG,CAAC,CAAC;IACpD,MAAM,eAAe,GAAG,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAC1D,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,IAAY;IACzC,IAAI,OAAO,GAAG,IAAI,CAAC;IACnB,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;YAClC,IAAI,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;gBACpB,OAAO,OAAO,CAAC;YACjB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QAEV,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,GAAG,MAAM,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function normalizePath(path: string): string;
2
+ //# sourceMappingURL=path-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/path-utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD"}
@@ -0,0 +1,4 @@
1
+ export function normalizePath(path) {
2
+ return path.replace(/\\/g, "/");
3
+ }
4
+ //# sourceMappingURL=path-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../../../src/utils/path-utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Request } from "express";
2
+ export declare const getRequestBaseUrl: (request: Request) => string;
3
+ //# sourceMappingURL=request-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/request-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,eAAO,MAAM,iBAAiB,YAAa,OAAO,KAAG,MACL,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const getRequestBaseUrl = (request) => `${request.protocol}://${request.get("host")}`;
2
+ //# sourceMappingURL=request-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-utils.js","sourceRoot":"","sources":["../../../src/utils/request-utils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAgB,EAAU,EAAE,CAC5D,GAAG,OAAO,CAAC,QAAQ,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC"}
@@ -0,0 +1,46 @@
1
+ import type {
2
+ DecoratorContext,
3
+ Interface,
4
+ Model,
5
+ Namespace,
6
+ Operation,
7
+ Type,
8
+ } from "@typespec/compiler";
9
+
10
+ /**
11
+ * Setup the boilerplate for a scenario service(server endpoint, etc.)
12
+ */
13
+ export type ScenarioServiceDecorator = (
14
+ context: DecoratorContext,
15
+ target: Namespace,
16
+ route: string,
17
+ options?: Type,
18
+ ) => void;
19
+
20
+ /**
21
+ * Mark an operation, interface or namespace as a scenario. All containing operations will be part of the same scenario.
22
+ */
23
+ export type ScenarioDecorator = (
24
+ context: DecoratorContext,
25
+ target: Namespace | Interface | Operation,
26
+ name?: string,
27
+ ) => void;
28
+
29
+ /**
30
+ * Specify documentation on how to implement this scenario.
31
+ *
32
+ * @param doc Documentation
33
+ * @param formatArgs Format arguments
34
+ */
35
+ export type ScenarioDocDecorator = (
36
+ context: DecoratorContext,
37
+ target: Namespace | Interface | Operation,
38
+ doc: string,
39
+ formatArgs?: Model,
40
+ ) => void;
41
+
42
+ export type TypeSpecSpectorDecorators = {
43
+ scenarioService: ScenarioServiceDecorator;
44
+ scenario: ScenarioDecorator;
45
+ scenarioDoc: ScenarioDocDecorator;
46
+ };
@@ -0,0 +1,5 @@
1
+ /** An error here would mean that the decorator is not exported or doesn't have the right name. */
2
+ import { $decorators } from "@typespec/spector";
3
+ import type { TypeSpecSpectorDecorators } from "./TypeSpec.Spector.js";
4
+ /** An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */
5
+ const _: TypeSpecSpectorDecorators = $decorators["TypeSpec.Spector"];
package/lib/main.tsp ADDED
@@ -0,0 +1,37 @@
1
+ import "../dist/src/lib/tsp-index.js";
2
+
3
+ using TypeSpec.Reflection;
4
+
5
+ namespace TypeSpec.Spector;
6
+
7
+ alias ScenarioServiceOptions = {
8
+ /**
9
+ * Version enum that would be used on the $versioned decorator
10
+ */
11
+ versioned?: Enum;
12
+ };
13
+
14
+ /**
15
+ * Setup the boilerplate for a scenario service(server endpoint, etc.)
16
+ */
17
+ extern dec scenarioService(
18
+ target: Namespace,
19
+ route: valueof string,
20
+ options?: ScenarioServiceOptions
21
+ );
22
+
23
+ /**
24
+ * Mark an operation, interface or namespace as a scenario. All containing operations will be part of the same scenario.
25
+ */
26
+ extern dec scenario(target: Namespace | Interface | Operation, name?: valueof string);
27
+
28
+ /**
29
+ * Specify documentation on how to implement this scenario.
30
+ * @param doc Documentation
31
+ * @param formatArgs Format arguments
32
+ */
33
+ extern dec scenarioDoc(
34
+ target: Namespace | Interface | Operation,
35
+ doc: valueof string,
36
+ formatArgs?: Model
37
+ );
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@typespec/spector",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Typespec Core Tool to validate, run mock api, collect coverage.",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/src/index.js",
8
+ "typespec": "./lib/main.tsp"
9
+ }
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "tsp-spector": "./cmd/cli.mjs"
14
+ },
15
+ "engines": {
16
+ "node": ">=16.0.0"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/microsoft/typespec.git"
21
+ },
22
+ "author": "Microsoft",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/microsoft/typespec/issues"
26
+ },
27
+ "homepage": "https://github.com/microsoft/typespec#readme",
28
+ "dependencies": {
29
+ "@azure/identity": "~4.4.1",
30
+ "@types/js-yaml": "^4.0.5",
31
+ "ajv": "~8.17.1",
32
+ "axios": "^1.7.5",
33
+ "body-parser": "^1.20.3",
34
+ "deep-equal": "^2.2.0",
35
+ "express": "^4.21.1",
36
+ "express-promise-router": "^4.1.1",
37
+ "form-data": "^4.0.1",
38
+ "globby": "~14.0.2",
39
+ "jackspeak": "4.0.2",
40
+ "js-yaml": "^4.1.0",
41
+ "morgan": "^1.10.0",
42
+ "multer": "^1.4.5-lts.1",
43
+ "node-fetch": "^3.3.1",
44
+ "picocolors": "~1.1.0",
45
+ "source-map-support": "~0.5.21",
46
+ "winston": "^3.15.0",
47
+ "xml2js": "^0.6.2",
48
+ "yargs": "~17.7.2",
49
+ "@typespec/compiler": "~0.61.2",
50
+ "@typespec/http": "~0.61.0",
51
+ "@typespec/versioning": "~0.61.0",
52
+ "@typespec/rest": "~0.61.0",
53
+ "@typespec/spec-api": "~0.1.0-alpha.0",
54
+ "@typespec/spec-coverage-sdk": "~0.1.0-alpha.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/body-parser": "^1.19.2",
58
+ "@types/deep-equal": "^1.0.1",
59
+ "@types/express": "^5.0.0",
60
+ "@types/morgan": "^1.9.4",
61
+ "@types/multer": "^1.4.10",
62
+ "@types/node": "~22.7.5",
63
+ "@types/node-fetch": "^2.6.3",
64
+ "@types/xml2js": "^0.4.11",
65
+ "@types/yargs": "~17.0.33",
66
+ "rimraf": "~6.0.1",
67
+ "typescript": "~5.6.3",
68
+ "@typespec/tspd": "~0.46.0"
69
+ },
70
+ "scripts": {
71
+ "watch": "tsc -p ./tsconfig.build.json --watch",
72
+ "build": "npm run gen-extern-signature && tsc -p tsconfig.build.json",
73
+ "clean": "rimraf dist/ temp/",
74
+ "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .",
75
+ "test": "vitest run",
76
+ "test:watch": "vitest -w",
77
+ "test:ui": "vitest --ui"
78
+ }
79
+ }
@@ -0,0 +1,98 @@
1
+ import { CoverageReport, ScenarioStatus } from "@typespec/spec-coverage-sdk";
2
+ import { readFile, writeFile } from "fs/promises";
3
+ import { loadSpecConfig } from "../config/config.js";
4
+ import { createCoverageReport } from "../coverage/coverage-report.js";
5
+ import { loadScenarioMockApis } from "../scenarios-resolver.js";
6
+ import { createDiagnosticReporter, findFilesFromPattern } from "../utils/index.js";
7
+
8
+ export interface CheckCoverageConfig {
9
+ scenariosPath: string;
10
+ configFile?: string;
11
+ coverageFiles: string[];
12
+ mergedCoverageFile: string;
13
+ ignoreNotImplemented?: boolean;
14
+ exitDueToPreviousError?: boolean;
15
+ hasMoreScenarios?: boolean;
16
+ }
17
+
18
+ export async function checkCoverage(config: CheckCoverageConfig): Promise<boolean> {
19
+ const inputCoverageFiles = (
20
+ await Promise.all(config.coverageFiles.map((x) => findFilesFromPattern(x)))
21
+ ).flat();
22
+
23
+ const results: Record<string, ScenarioStatus> = {};
24
+ const diagnosticsReporter = createDiagnosticReporter();
25
+ const scenarios = await loadScenarioMockApis(config.scenariosPath);
26
+
27
+ for (const scenarioName of Object.keys(scenarios)) {
28
+ results[scenarioName] = "not-implemented";
29
+ }
30
+
31
+ if (config.configFile) {
32
+ const [specConfig, diagnostics] = await loadSpecConfig(config.configFile);
33
+ diagnosticsReporter.reportDiagnostics(diagnostics);
34
+
35
+ for (const scenarioName of specConfig.unsupportedScenarios) {
36
+ results[scenarioName] = "not-supported";
37
+ }
38
+ }
39
+
40
+ for (const coverageFile of inputCoverageFiles) {
41
+ const content = await readFile(coverageFile);
42
+ const inputCoverage: CoverageReport = JSON.parse(content.toString());
43
+
44
+ for (const [scenarioName, scenarioStatus] of Object.entries(inputCoverage.results)) {
45
+ const existing = results[scenarioName];
46
+ if (existing === undefined) {
47
+ diagnosticsReporter.reportDiagnostic({
48
+ message: `Scenario ${scenarioName} with coverage in file "${coverageFile}" is not defined in the scenarios in path "${config.scenariosPath}".`,
49
+ });
50
+ continue;
51
+ }
52
+
53
+ switch (scenarioStatus) {
54
+ case "fail":
55
+ results[scenarioName] = "fail";
56
+ diagnosticsReporter.reportDiagnostic({
57
+ message: `Scenario ${scenarioName} failed in "${coverageFile}".`,
58
+ });
59
+ break;
60
+ case "pass":
61
+ case "not-applicable":
62
+ case "not-supported":
63
+ if (existing === "not-implemented") {
64
+ results[scenarioName] = scenarioStatus;
65
+ }
66
+ break;
67
+ case "not-implemented":
68
+ // nothing
69
+ }
70
+ }
71
+ }
72
+
73
+ if (!config.ignoreNotImplemented) {
74
+ for (const [scenarioName, scenarioStatus] of Object.entries(results)) {
75
+ if (scenarioStatus === "not-implemented") {
76
+ diagnosticsReporter.reportDiagnostic({
77
+ message: `Scenario ${scenarioName} is not implemented.`,
78
+ });
79
+ }
80
+ }
81
+ }
82
+
83
+ const coverageReport = createCoverageReport(config.scenariosPath, results);
84
+ await writeFile(config.mergedCoverageFile, JSON.stringify(coverageReport, null, 2));
85
+
86
+ if (diagnosticsReporter.diagnostics.length === 0) {
87
+ if (config.exitDueToPreviousError) {
88
+ process.exit(1);
89
+ }
90
+ return false;
91
+ } else {
92
+ if (config.hasMoreScenarios) {
93
+ return true;
94
+ } else {
95
+ process.exit(1);
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,63 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import pc from "picocolors";
3
+ import prettier from "prettier";
4
+ import type { Scenario, ScenarioEndpoint } from "../lib/decorators.js";
5
+ import { logger } from "../logger.js";
6
+ import { loadScenarios } from "../scenarios-resolver.js";
7
+
8
+ export interface GenerateScenarioSummaryConfig {
9
+ scenariosPath: string;
10
+ outputFile: string;
11
+ overrideOutputFile?: boolean;
12
+ }
13
+
14
+ export async function generateScenarioSummary({
15
+ scenariosPath,
16
+ outputFile,
17
+ overrideOutputFile,
18
+ }: GenerateScenarioSummaryConfig) {
19
+ const [scenarios, diagnostics] = await loadScenarios(scenariosPath);
20
+
21
+ if (diagnostics.length > 0) {
22
+ process.exit(-1);
23
+ }
24
+
25
+ let summary = await createScenarioSummary(scenarios);
26
+ if (overrideOutputFile) {
27
+ const existingContent = await readFile(outputFile, "utf-8");
28
+ summary = summary.replace(/# Spec Project summary/, "");
29
+ await writeFile(outputFile, `${existingContent}\n${summary}`);
30
+ } else {
31
+ await writeFile(outputFile, summary);
32
+ }
33
+ logger.info(`${pc.green("✓")} Scenario summary generated at ${outputFile}.`);
34
+ }
35
+
36
+ export function createScenarioSummary(scenarios: Scenario[]): Promise<string> {
37
+ const lines = [`# Spec Project summary`];
38
+
39
+ for (const scenario of scenarios.sort((a, b) => a.name.localeCompare(b.name))) {
40
+ lines.push(`### ${scenario.name}`);
41
+ lines.push("");
42
+ const endpoints = renderEndpoints(scenario.endpoints);
43
+ if (endpoints) {
44
+ lines.push(...endpoints);
45
+ }
46
+ lines.push("");
47
+ lines.push(`${scenario.scenarioDoc}`);
48
+ lines.push("");
49
+ }
50
+ const markdown = lines.join("\n");
51
+
52
+ return prettier.format(markdown, { parser: "markdown" });
53
+ }
54
+
55
+ function renderEndpoints(endpoints: ScenarioEndpoint[]) {
56
+ if (endpoints.length === 0) {
57
+ return undefined;
58
+ } else if (endpoints.length === 1) {
59
+ return [`- Endpoint: \`${endpoints[0].verb} ${endpoints[0].path}\``];
60
+ } else {
61
+ return [`- Endpoints:`, ...endpoints.map((x) => ` - \`${endpoints[0].verb} ${x.path}\``)];
62
+ }
63
+ }
@@ -0,0 +1,116 @@
1
+ import { HttpMethod, ServiceRequestFile } from "@typespec/spec-api";
2
+ import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
3
+ import FormData from "form-data";
4
+
5
+ export interface ServiceRequest {
6
+ endPoint: string;
7
+ options?: {
8
+ requestBody?: any;
9
+ files?: ServiceRequestFile[];
10
+ config?: AxiosRequestConfig<any> | undefined;
11
+ };
12
+ }
13
+
14
+ function checkAndAddFormDataIfRequired(request: ServiceRequest) {
15
+ if (request.options?.config?.headers?.["Content-Type"] === "multipart/form-data") {
16
+ const formData = new FormData();
17
+ if (request.options?.requestBody) {
18
+ for (const key in request.options.requestBody) {
19
+ formData.append(key, JSON.stringify(request.options.requestBody[key]));
20
+ }
21
+ }
22
+ if (request.options.files) {
23
+ request.options.files.forEach((file) => {
24
+ formData.append(`${file.fieldname}`, file.buffer, {
25
+ filename: file.originalname,
26
+ contentType: file.mimetype,
27
+ });
28
+ });
29
+ }
30
+ request.options.requestBody = formData;
31
+ request.options.config = {
32
+ ...request.options.config,
33
+ headers: formData.getHeaders(),
34
+ };
35
+ }
36
+ }
37
+
38
+ function checkAndUpdateEndpoint(request: ServiceRequest) {
39
+ if (request.options?.config?.params) {
40
+ for (const key in request.options.config.params) {
41
+ request.endPoint = request.endPoint.replace(`:${key}`, request.options.config.params[key]);
42
+ }
43
+ }
44
+ request.endPoint = request.endPoint.replace(/\[:\]/g, ":");
45
+ }
46
+
47
+ export async function makeServiceCall(
48
+ serviceCallType: HttpMethod,
49
+ request: ServiceRequest,
50
+ ): Promise<AxiosResponse<any, any>> {
51
+ checkAndUpdateEndpoint(request);
52
+ checkAndAddFormDataIfRequired(request);
53
+ if (serviceCallType === "put") {
54
+ return await makePutCall(request);
55
+ }
56
+ if (serviceCallType === "post") {
57
+ return await makePostCall(request);
58
+ }
59
+ if (serviceCallType === "get") {
60
+ return await makeGetCall(request);
61
+ }
62
+ if (serviceCallType === "delete") {
63
+ return await makeDeleteCall(request);
64
+ }
65
+ if (serviceCallType === "head") {
66
+ return await makeHeadCall(request);
67
+ }
68
+ return await makePatchCall(request);
69
+ }
70
+
71
+ export async function makePutCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
72
+ const response = await axios.put(
73
+ request.endPoint,
74
+ request.options?.requestBody,
75
+ request.options?.config,
76
+ );
77
+ return response;
78
+ }
79
+
80
+ export async function makePostCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
81
+ const response = await axios.post(
82
+ request.endPoint,
83
+ request.options?.requestBody,
84
+ request.options?.config,
85
+ );
86
+ return response;
87
+ }
88
+
89
+ export async function makeGetCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
90
+ const response = await axios.get(request.endPoint, request.options?.config);
91
+ return response;
92
+ }
93
+
94
+ export async function makePatchCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
95
+ const response = await axios.patch(
96
+ request.endPoint,
97
+ request.options?.requestBody,
98
+ request.options?.config,
99
+ );
100
+ return response;
101
+ }
102
+
103
+ export async function makeDeleteCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
104
+ const response = await axios.delete(request.endPoint, request.options?.config);
105
+ return response;
106
+ }
107
+
108
+ export async function makeHeadCall(request: ServiceRequest): Promise<AxiosResponse<any, any>> {
109
+ const response = await axios.head(request.endPoint, request.options?.config);
110
+ return response;
111
+ }
112
+
113
+ type EncodingType = "utf-8" | "base64" | "base64url" | "hex";
114
+ export function uint8ArrayToString(bytes: Uint8Array, format: EncodingType): string {
115
+ return Buffer.from(bytes).toString(format);
116
+ }
@@ -0,0 +1 @@
1
+ export * from "./validate-scenarios.js";
@@ -0,0 +1,88 @@
1
+ import { spawn } from "child_process";
2
+ import fetch from "node-fetch";
3
+ import { resolve } from "path";
4
+ import { MockApiApp } from "../app/app.js";
5
+ import { AdminUrls } from "../constants.js";
6
+ import { logger } from "../logger.js";
7
+ import { ensureScenariosPathExists } from "../utils/index.js";
8
+
9
+ export interface ServeConfig {
10
+ scenariosPath: string | string[];
11
+ coverageFile: string;
12
+ port: number;
13
+ }
14
+
15
+ export interface StopConfig {
16
+ port: number;
17
+ }
18
+
19
+ export async function serve(config: ServeConfig) {
20
+ if (Array.isArray(config.scenariosPath)) {
21
+ for (let idx = 0; idx < config.scenariosPath.length; idx++) {
22
+ config.scenariosPath[idx] = resolve(process.cwd(), config.scenariosPath[idx]);
23
+ await ensureScenariosPathExists(config.scenariosPath[idx]);
24
+ }
25
+ } else {
26
+ await ensureScenariosPathExists(config.scenariosPath);
27
+ }
28
+
29
+ const server = new MockApiApp({
30
+ port: config.port,
31
+ scenarioPath: config.scenariosPath,
32
+ coverageFile: config.coverageFile,
33
+ });
34
+ await server.start();
35
+ }
36
+
37
+ export async function startInBackground(config: ServeConfig) {
38
+ return new Promise<void>((resolve) => {
39
+ const [nodeExe, entrypoint] = process.argv;
40
+ logger.info(`Starting server in background at port ${config.port}`);
41
+ const scenariosPath = Array.isArray(config.scenariosPath)
42
+ ? config.scenariosPath.join(" ")
43
+ : config.scenariosPath;
44
+ const cp = spawn(
45
+ nodeExe,
46
+ [
47
+ entrypoint,
48
+ "serve",
49
+ scenariosPath,
50
+ "--port",
51
+ config.port.toString(),
52
+ "--coverageFile",
53
+ config.coverageFile,
54
+ ],
55
+ {
56
+ detached: true,
57
+ stdio: "ignore",
58
+ },
59
+ );
60
+ const exitListener = (exitCode: number) => {
61
+ logger.error(`Server exited within 1s with exit code ${exitCode}`);
62
+ process.exit(1);
63
+ };
64
+ cp.on("exit", exitListener);
65
+ setTimeout(() => {
66
+ cp.removeListener("exit", exitListener);
67
+ logger.info(`Stated server with pid: ${cp.pid}`);
68
+ cp.unref();
69
+ resolve();
70
+ }, 1000);
71
+ });
72
+ }
73
+
74
+ export async function stop(config: StopConfig) {
75
+ try {
76
+ await fetch(`http://localhost:${config.port}${AdminUrls.stop}`, {
77
+ method: "post",
78
+ });
79
+
80
+ logger.info(`Stopped server running at port ${config.port}.`);
81
+ } catch (e: any) {
82
+ if (e.code === "ECONNREFUSED") {
83
+ logger.info(`No server running at port ${config.port}.`);
84
+ } else {
85
+ throw e;
86
+ }
87
+ }
88
+ }