counterfact 2.5.0 → 2.6.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 (43) hide show
  1. package/README.md +1 -0
  2. package/bin/README.md +1 -0
  3. package/bin/counterfact.js +164 -23
  4. package/bin/register-ts-loader.mjs +17 -0
  5. package/bin/ts-loader.mjs +31 -0
  6. package/dist/app.js +23 -12
  7. package/dist/migrate/update-route-types.js +47 -29
  8. package/dist/repl/raw-http-client.js +14 -14
  9. package/dist/repl/repl.js +24 -2
  10. package/dist/repl/route-builder.js +270 -0
  11. package/dist/server/config.js +1 -1
  12. package/dist/server/context-registry.js +27 -3
  13. package/dist/server/counterfact-types/index.ts +11 -1
  14. package/dist/server/determine-module-kind.js +1 -1
  15. package/dist/server/dispatcher.js +21 -10
  16. package/dist/server/file-discovery.js +34 -0
  17. package/dist/server/middleware-detector.js +8 -0
  18. package/dist/server/module-dependency-graph.js +4 -1
  19. package/dist/server/module-loader.js +7 -31
  20. package/dist/server/module-tree.js +26 -23
  21. package/dist/server/openapi-middleware.js +2 -2
  22. package/dist/server/registry.js +2 -2
  23. package/dist/server/request-validator.js +61 -0
  24. package/dist/server/transpiler.js +13 -5
  25. package/dist/typescript-generator/coder.js +8 -4
  26. package/dist/typescript-generator/generate.js +3 -3
  27. package/dist/typescript-generator/jsdoc.js +45 -0
  28. package/dist/typescript-generator/operation-coder.js +8 -5
  29. package/dist/typescript-generator/operation-type-coder.js +21 -11
  30. package/dist/typescript-generator/parameter-export-type-coder.js +4 -1
  31. package/dist/typescript-generator/parameters-type-coder.js +6 -1
  32. package/dist/typescript-generator/prune.js +11 -11
  33. package/dist/typescript-generator/repository.js +1 -1
  34. package/dist/typescript-generator/requirement.js +10 -5
  35. package/dist/typescript-generator/response-type-coder.js +10 -5
  36. package/dist/typescript-generator/responses-type-coder.js +1 -0
  37. package/dist/typescript-generator/schema-coder.js +5 -5
  38. package/dist/typescript-generator/schema-type-coder.js +23 -12
  39. package/dist/typescript-generator/script.js +18 -5
  40. package/dist/typescript-generator/specification.js +13 -4
  41. package/dist/util/ensure-directory-exists.js +1 -1
  42. package/dist/util/runtime-can-execute-erasable-ts.js +22 -0
  43. package/package.json +7 -6
@@ -2,16 +2,19 @@ import { printObject } from "./printers.js";
2
2
  import { SchemaTypeCoder } from "./schema-type-coder.js";
3
3
  import { TypeCoder } from "./type-coder.js";
4
4
  export class ResponseTypeCoder extends TypeCoder {
5
+ openApi2MediaTypes;
5
6
  constructor(requirement, openApi2MediaTypes = []) {
6
7
  super(requirement);
7
8
  this.openApi2MediaTypes = openApi2MediaTypes;
8
9
  }
9
10
  names() {
10
- return super.names(this.requirement.data.$ref.split("/").at(-1));
11
+ return super.names(this.requirement.data["$ref"].split("/").at(-1));
11
12
  }
12
13
  buildContentObjectType(script, response) {
13
14
  if (response.has("content")) {
14
- return response.get("content").map((content, mediaType) => [
15
+ return response
16
+ .get("content")
17
+ .map((content, mediaType) => [
15
18
  mediaType,
16
19
  `{
17
20
  schema: ${content.has("schema") ? new SchemaTypeCoder(content.get("schema")).write(script) : "unknown"}
@@ -46,8 +49,10 @@ export class ResponseTypeCoder extends TypeCoder {
46
49
  return printObject(this.buildHeaders(script, response));
47
50
  }
48
51
  printRequiredHeaders(response) {
49
- const requiredHeaders = (response.get("headers") ?? [])
50
- .map((value, name) => ({ name, required: value.data.required }))
52
+ const requiredHeaders = (response.get("headers")?.map((value, name) => ({
53
+ name,
54
+ required: value.data.required,
55
+ })) ?? [])
51
56
  .filter(({ required }) => required)
52
57
  .map(({ name }) => `"${name}"`);
53
58
  return requiredHeaders.length === 0 ? "never" : requiredHeaders.join(" | ");
@@ -72,7 +77,7 @@ export class ResponseTypeCoder extends TypeCoder {
72
77
  return printObject(exampleNames.map((name) => [name, "unknown"]));
73
78
  }
74
79
  modulePath() {
75
- return `types/${this.requirement.data.$ref}.ts`;
80
+ return `types/${this.requirement.data["$ref"]}.ts`;
76
81
  }
77
82
  writeCode(script) {
78
83
  return `{
@@ -2,6 +2,7 @@ import { printObjectWithoutQuotes } from "./printers.js";
2
2
  import { ResponseTypeCoder } from "./response-type-coder.js";
3
3
  import { TypeCoder } from "./type-coder.js";
4
4
  export class ResponsesTypeCoder extends TypeCoder {
5
+ openApi2MediaTypes;
5
6
  constructor(requirement, openApi2MediaTypes = []) {
6
7
  super(requirement);
7
8
  this.openApi2MediaTypes = openApi2MediaTypes;
@@ -2,13 +2,13 @@ import { Coder } from "./coder.js";
2
2
  function scrubSchema(schema) {
3
3
  // remove properties that are not valid in JSON Schema 6 and not useful anyway
4
4
  const cleaned = { ...schema };
5
- delete cleaned.example;
6
- delete cleaned.xml;
5
+ delete cleaned["example"];
6
+ delete cleaned["xml"];
7
7
  return cleaned;
8
8
  }
9
9
  export class SchemaCoder extends Coder {
10
10
  names() {
11
- return super.names(`${this.requirement.data.$ref.split("/").at(-1)}Schema`);
11
+ return super.names(`${this.requirement.data["$ref"].split("/").at(-1)}Schema`);
12
12
  }
13
13
  objectSchema(script) {
14
14
  const { properties, required } = this.requirement.data;
@@ -30,11 +30,11 @@ export class SchemaCoder extends Coder {
30
30
  items: ${new SchemaCoder(this.requirement.get("items")).write(script)}
31
31
  }`;
32
32
  }
33
- typeDeclaration(namespace, script) {
33
+ typeDeclaration(_namespace, script) {
34
34
  return script.importExternalType("JSONSchema6", "json-schema");
35
35
  }
36
36
  modulePath() {
37
- return `types/${this.requirement.data.$ref}.ts`;
37
+ return `types/${this.requirement.data["$ref"]}.ts`;
38
38
  }
39
39
  writeCode(script) {
40
40
  const { type } = this.requirement.data;
@@ -1,7 +1,11 @@
1
+ import { buildJsDoc } from "./jsdoc.js";
1
2
  import { TypeCoder } from "./type-coder.js";
2
3
  export class SchemaTypeCoder extends TypeCoder {
3
4
  names() {
4
- return super.names(this.requirement.data.$ref.split("/").at(-1));
5
+ return super.names(this.requirement.data["$ref"]?.split("/").at(-1));
6
+ }
7
+ jsdoc() {
8
+ return buildJsDoc(this.requirement.data);
5
9
  }
6
10
  additionalPropertiesType(script) {
7
11
  const { additionalProperties, properties } = this.requirement.data;
@@ -16,13 +20,17 @@ export class SchemaTypeCoder extends TypeCoder {
16
20
  }
17
21
  objectSchema(script) {
18
22
  const { data } = this.requirement;
19
- const properties = Object.keys(data.properties ?? {}).map((name) => {
23
+ const typedData = data;
24
+ const properties = Object.keys(typedData.properties ?? {}).map((name) => {
20
25
  const property = this.requirement.get("properties").get(name);
21
- const isRequired = data.required?.includes(name) || property.data.required === true;
26
+ const propertyData = property.data;
27
+ const isRequired = typedData.required?.includes(name) || propertyData.required === true;
22
28
  const optionalFlag = isRequired ? "" : "?";
23
- return `"${name}"${optionalFlag}: ${new SchemaTypeCoder(property).write(script)}`;
29
+ const comment = buildJsDoc(property.data);
30
+ const commentPrefix = comment ? `\n${comment}` : "";
31
+ return `${commentPrefix}"${name}"${optionalFlag}: ${new SchemaTypeCoder(property).write(script)}`;
24
32
  });
25
- if (data.additionalProperties) {
33
+ if (typedData.additionalProperties) {
26
34
  properties.push(`[key: string]: ${this.additionalPropertiesType(script)}`);
27
35
  }
28
36
  return `{${properties.join(",")}}`;
@@ -37,11 +45,13 @@ export class SchemaTypeCoder extends TypeCoder {
37
45
  if (value === null) {
38
46
  return "null";
39
47
  }
40
- return value;
48
+ return String(value);
41
49
  }
42
50
  writeType(script, type) {
43
51
  if (Array.isArray(type)) {
44
- return type.map((item) => this.writeType(script, item)).join(" | ");
52
+ return type
53
+ .map((item) => this.writeType(script, item))
54
+ .join(" | ");
45
55
  }
46
56
  if (typeof type !== "string") {
47
57
  return "unknown";
@@ -57,7 +67,7 @@ export class SchemaTypeCoder extends TypeCoder {
57
67
  }
58
68
  return type ?? "unknown";
59
69
  }
60
- writeGroup(script, { allOf, anyOf, oneOf }) {
70
+ writeGroup(script, { allOf, anyOf, oneOf, }) {
61
71
  function matchingKey() {
62
72
  if (allOf) {
63
73
  return "allOf";
@@ -67,19 +77,20 @@ export class SchemaTypeCoder extends TypeCoder {
67
77
  }
68
78
  return "oneOf";
69
79
  }
70
- const types = (allOf ?? anyOf ?? oneOf).map((item, index) => new SchemaTypeCoder(this.requirement.get(matchingKey()).get(index)).write(script));
80
+ const key = matchingKey();
81
+ const items = (allOf ?? anyOf ?? oneOf);
82
+ const types = items.map((_item, index) => new SchemaTypeCoder(this.requirement.get(key).get(index)).write(script));
71
83
  return types.join(allOf ? " & " : " | ");
72
84
  }
73
- writeEnum(script, requirement) {
85
+ writeEnum(_script, requirement) {
74
86
  return requirement.data
75
87
  .map((item) => this.writePrimitive(item))
76
88
  .join(" | ");
77
89
  }
78
90
  modulePath() {
79
- return `types/${this.requirement.data.$ref.replace(/^#\//u, "")}.ts`;
91
+ return `types/${this.requirement.data["$ref"].replace(/^#\//u, "")}.ts`;
80
92
  }
81
93
  writeCode(script) {
82
- // script.comments = READ_ONLY_COMMENTS;
83
94
  const { allOf, anyOf, oneOf, type, format } = this.requirement.data;
84
95
  if (allOf ?? anyOf ?? oneOf) {
85
96
  return this.writeGroup(script, { allOf, anyOf, oneOf });
@@ -4,6 +4,14 @@ import { format } from "prettier";
4
4
  import { escapePathForWindows } from "../util/windows-escape.js";
5
5
  const debug = createDebugger("counterfact:typescript-generator:script");
6
6
  export class Script {
7
+ repository;
8
+ comments;
9
+ exports;
10
+ imports;
11
+ externalImport;
12
+ cache;
13
+ typeCache;
14
+ path;
7
15
  constructor(repository, path) {
8
16
  this.repository = repository;
9
17
  this.comments = [];
@@ -42,6 +50,7 @@ export class Script {
42
50
  id: coder.id,
43
51
  isDefault,
44
52
  isType,
53
+ jsdoc: "",
45
54
  typeDeclaration: coder.typeDeclaration(this.exports, this),
46
55
  };
47
56
  exportStatement.promise = coder
@@ -49,11 +58,13 @@ export class Script {
49
58
  .then((availableCoder) => {
50
59
  exportStatement.name = name;
51
60
  exportStatement.code = availableCoder.write(this);
61
+ exportStatement.jsdoc = availableCoder.jsdoc();
52
62
  return availableCoder;
53
63
  })
54
64
  .catch((error) => {
55
65
  exportStatement.code = `{/* error creating export "${name}" for ${this.path}: ${error.stack} */}`;
56
66
  exportStatement.error = error;
67
+ return undefined;
57
68
  })
58
69
  .finally(() => {
59
70
  exportStatement.done = true;
@@ -125,16 +136,18 @@ export class Script {
125
136
  });
126
137
  }
127
138
  exportStatements() {
128
- return Array.from(this.exports.values(), ({ beforeExport, code, isDefault, isType, name, typeDeclaration }) => {
129
- if (code.raw) {
139
+ return Array.from(this.exports.values(), ({ beforeExport, code, isDefault, isType, jsdoc, name, typeDeclaration, }) => {
140
+ if (typeof code === "object" && code !== null && "raw" in code) {
130
141
  return code.raw;
131
142
  }
132
143
  if (isDefault) {
133
- return `${beforeExport}export default ${code};`;
144
+ return `${jsdoc}${beforeExport}export default ${code};`;
134
145
  }
135
146
  const keyword = isType ? "type" : "const";
136
- const typeAnnotation = typeDeclaration.length === 0 ? "" : `:${typeDeclaration}`;
137
- return `${beforeExport}export ${keyword} ${name}${typeAnnotation} = ${code};`;
147
+ const typeAnnotation = (typeDeclaration ?? "").length === 0
148
+ ? ""
149
+ : `:${typeDeclaration ?? ""}`;
150
+ return `${jsdoc}${beforeExport}export ${keyword} ${name ?? ""}${typeAnnotation} = ${code};`;
138
151
  });
139
152
  }
140
153
  contents() {
@@ -1,12 +1,15 @@
1
+ import { bundle } from "@apidevtools/json-schema-ref-parser";
1
2
  import createDebug from "debug";
2
3
  import { Requirement } from "./requirement.js";
3
- import { bundle } from "@apidevtools/json-schema-ref-parser";
4
4
  const debug = createDebug("counterfact:typescript-generator:specification");
5
5
  export class Specification {
6
+ cache;
7
+ rootRequirement;
6
8
  constructor(rootRequirement) {
7
9
  this.cache = new Map();
8
- this.rootUrl = rootRequirement;
9
- this.rootRequirement = rootRequirement;
10
+ if (rootRequirement) {
11
+ this.rootRequirement = rootRequirement;
12
+ }
10
13
  }
11
14
  static async fromFile(urlOrPath) {
12
15
  const specification = new Specification();
@@ -18,6 +21,12 @@ export class Specification {
18
21
  return this.rootRequirement.select(url.slice(2));
19
22
  }
20
23
  async load(urlOrPath) {
21
- this.rootRequirement = new Requirement(await bundle(urlOrPath), urlOrPath, this);
24
+ try {
25
+ this.rootRequirement = new Requirement((await bundle(urlOrPath)), urlOrPath, this);
26
+ }
27
+ catch (error) {
28
+ const details = error instanceof Error ? error.message : String(error);
29
+ throw new Error(`Could not load the OpenAPI spec from "${urlOrPath}".\n${details}`, { cause: error });
30
+ }
22
31
  }
23
32
  }
@@ -3,7 +3,7 @@ import nodePath from "node:path";
3
3
  export function ensureDirectoryExists(filePath) {
4
4
  const directory = nodePath.dirname(filePath);
5
5
  try {
6
- fs.accessSync(directory, fs.fsConstants.W_OK);
6
+ fs.accessSync(directory, fs.constants.W_OK);
7
7
  }
8
8
  catch {
9
9
  // with the async option, await doesn't seem to wait for the directory to be created
@@ -0,0 +1,22 @@
1
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ export async function runtimeCanExecuteErasableTs() {
6
+ const dir = mkdtempSync(join(tmpdir(), "ts-probe-"));
7
+ // helper.ts is imported via .js extension — the TypeScript convention used
8
+ // throughout this codebase. If the runtime resolves helper.js → helper.ts,
9
+ // it is fully capable of running the TypeScript source tree.
10
+ writeFileSync(join(dir, "helper.ts"), 'export const value: string = "ok";\n', "utf8");
11
+ writeFileSync(join(dir, "main.ts"), 'import { value } from "./helper.js"; export default value;\n', "utf8");
12
+ try {
13
+ const mod = await import(pathToFileURL(join(dir, "main.ts")).href);
14
+ return mod?.default === "ok";
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ finally {
20
+ rmSync(dir, { recursive: true, force: true });
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "counterfact",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.",
5
5
  "type": "module",
6
6
  "main": "./dist/app.js",
@@ -94,10 +94,9 @@
94
94
  "@types/koa-bodyparser": "4.3.13",
95
95
  "@types/koa-proxy": "1.0.8",
96
96
  "@types/koa-static": "4.0.4",
97
- "@types/lodash": "4.17.24",
98
97
  "@types/node": "22",
99
- "@typescript-eslint/eslint-plugin": "^8.53.0",
100
- "@typescript-eslint/parser": "^8.53.0",
98
+ "@typescript-eslint/eslint-plugin": "^8.58.0",
99
+ "@typescript-eslint/parser": "^8.58.0",
101
100
  "copyfiles": "2.4.1",
102
101
  "eslint": "10.1.0",
103
102
  "eslint-formatter-github-annotations": "0.1.0",
@@ -127,10 +126,10 @@
127
126
  "@apidevtools/json-schema-ref-parser": "13.0.5",
128
127
  "@hapi/accept": "6.0.3",
129
128
  "@types/json-schema": "7.0.15",
129
+ "ajv": "6.14.0",
130
130
  "chokidar": "5.0.0",
131
131
  "commander": "14.0.3",
132
132
  "debug": "4.4.3",
133
- "fetch": "1.1.0",
134
133
  "fs-extra": "11.3.4",
135
134
  "handlebars": "4.7.9",
136
135
  "http-terminator": "3.2.0",
@@ -141,12 +140,14 @@
141
140
  "koa-bodyparser": "4.4.1",
142
141
  "koa-proxies": "0.12.4",
143
142
  "koa2-swagger-ui": "5.12.0",
144
- "lodash": "4.18.1",
145
143
  "node-fetch": "3.3.2",
146
144
  "open": "11.0.0",
147
145
  "patch-package": "8.0.1",
146
+ "posthog-node": "^5.28.11",
148
147
  "precinct": "12.2.0",
149
148
  "prettier": "3.8.1",
149
+ "recast": "0.23.11",
150
+ "tsx": "^4.20.3",
150
151
  "typescript": "6.0.2"
151
152
  },
152
153
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",