counterfact 2.5.1 → 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 (34) 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 +30 -10
  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 +3 -0
  26. package/dist/typescript-generator/jsdoc.js +45 -0
  27. package/dist/typescript-generator/operation-type-coder.js +4 -0
  28. package/dist/typescript-generator/parameters-type-coder.js +5 -1
  29. package/dist/typescript-generator/prune.js +2 -1
  30. package/dist/typescript-generator/schema-type-coder.js +7 -1
  31. package/dist/typescript-generator/script.js +5 -3
  32. package/dist/typescript-generator/specification.js +7 -1
  33. package/dist/util/runtime-can-execute-erasable-ts.js +22 -0
  34. package/package.json +7 -6
@@ -3,8 +3,8 @@ function isDirectory(test) {
3
3
  }
4
4
  export class ModuleTree {
5
5
  root = {
6
- directories: {},
7
- files: {},
6
+ directories: new Map(),
7
+ files: new Map(),
8
8
  isWildcard: false,
9
9
  name: "",
10
10
  rawName: "",
@@ -17,16 +17,19 @@ export class ModuleTree {
17
17
  if (remainingSegments.length === 0) {
18
18
  return directory;
19
19
  }
20
- const isNewDirectory = directory.directories[segment.toLowerCase()] === undefined;
21
- const nextDirectory = (directory.directories[segment.toLowerCase()] ??= {
22
- directories: {},
23
- files: {},
24
- isWildcard: segment.startsWith("{"),
25
- name: segment.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
26
- rawName: segment,
27
- });
20
+ const isNewDirectory = !directory.directories.has(segment.toLowerCase());
21
+ if (isNewDirectory) {
22
+ directory.directories.set(segment.toLowerCase(), {
23
+ directories: new Map(),
24
+ files: new Map(),
25
+ isWildcard: segment.startsWith("{"),
26
+ name: segment.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
27
+ rawName: segment,
28
+ });
29
+ }
30
+ const nextDirectory = directory.directories.get(segment.toLowerCase());
28
31
  if (isNewDirectory && segment.startsWith("{")) {
29
- const ambiguousWildcardDirectories = Object.values(directory.directories).filter((subdirectory) => subdirectory.isWildcard);
32
+ const ambiguousWildcardDirectories = Array.from(directory.directories.values()).filter((subdirectory) => subdirectory.isWildcard);
30
33
  if (ambiguousWildcardDirectories.length > 1) {
31
34
  process.stderr.write(`[counterfact] ERROR: Ambiguous wildcard paths detected. Multiple wildcard directories exist at the same level: ${ambiguousWildcardDirectories.map((d) => d.rawName).join(", ")}. Requests may be routed unpredictably.\n`);
32
35
  }
@@ -42,14 +45,14 @@ export class ModuleTree {
42
45
  if (filename === undefined) {
43
46
  throw new Error("The file name (the last segment of the URL) is undefined. This is theoretically impossible but TypeScript can't enforce it.");
44
47
  }
45
- targetDirectory.files[filename] = {
48
+ targetDirectory.files.set(filename, {
46
49
  isWildcard: filename.startsWith("{"),
47
50
  module,
48
51
  name: filename.replace(/^\{(?<name>.*)\}$/u, "$<name>"),
49
52
  rawName: filename,
50
- };
53
+ });
51
54
  if (filename.startsWith("{")) {
52
- const ambiguousWildcardFiles = Object.values(targetDirectory.files).filter((file) => file.isWildcard);
55
+ const ambiguousWildcardFiles = Array.from(targetDirectory.files.values()).filter((file) => file.isWildcard);
53
56
  if (ambiguousWildcardFiles.length > 1) {
54
57
  process.stderr.write(`[counterfact] ERROR: Ambiguous wildcard paths detected. Multiple wildcard files exist at the same path level: ${ambiguousWildcardFiles.map((f) => f.rawName).join(", ")}. Requests may be routed unpredictably.\n`);
55
58
  }
@@ -67,10 +70,10 @@ export class ModuleTree {
67
70
  return;
68
71
  }
69
72
  if (remainingSegments.length === 0) {
70
- delete directory.files[segment.toLowerCase()];
73
+ directory.files.delete(segment.toLowerCase());
71
74
  return;
72
75
  }
73
- this.removeModuleFromDirectory(directory.directories[segment.toLowerCase()], remainingSegments);
76
+ this.removeModuleFromDirectory(directory.directories.get(segment.toLowerCase()), remainingSegments);
74
77
  }
75
78
  remove(url) {
76
79
  const segments = url.split("/").slice(1);
@@ -81,14 +84,14 @@ export class ModuleTree {
81
84
  }
82
85
  buildMatch(directory, segment, pathVariables, matchedPath, method) {
83
86
  function normalizedSegment(segment, directory) {
84
- for (const file in directory.files) {
87
+ for (const file of directory.files.keys()) {
85
88
  if (file.toLowerCase() === segment.toLowerCase()) {
86
89
  return file;
87
90
  }
88
91
  }
89
92
  return "";
90
93
  }
91
- const exactMatchFile = directory.files[normalizedSegment(segment, directory)];
94
+ const exactMatchFile = directory.files.get(normalizedSegment(segment, directory));
92
95
  // If the URL segment literally matches a file key (e.g., requesting "/{x}"
93
96
  // as a literal URL value), exactMatchFile may be a wildcard file. In that
94
97
  // case, fall through to wildcard matching below.
@@ -99,7 +102,7 @@ export class ModuleTree {
99
102
  pathVariables,
100
103
  };
101
104
  }
102
- const wildcardFiles = Object.values(directory.files).filter((file) => file.isWildcard && this.fileModuleDefined(file, method));
105
+ const wildcardFiles = Array.from(directory.files.values()).filter((file) => file.isWildcard && this.fileModuleDefined(file, method));
103
106
  if (wildcardFiles.length > 1) {
104
107
  const firstWildcard = wildcardFiles[0];
105
108
  return {
@@ -144,11 +147,11 @@ export class ModuleTree {
144
147
  (remainingSegments.length === 1 && remainingSegments[0] === "")) {
145
148
  return this.buildMatch(directory, segment, pathVariables, matchedPath, method);
146
149
  }
147
- const exactMatch = directory.directories[segment.toLowerCase()];
150
+ const exactMatch = directory.directories.get(segment.toLowerCase());
148
151
  if (isDirectory(exactMatch)) {
149
152
  return this.matchWithinDirectory(exactMatch, remainingSegments, pathVariables, `${matchedPath}/${segment}`, method);
150
153
  }
151
- const wildcardDirectories = Object.values(directory.directories).filter((subdirectory) => subdirectory.isWildcard);
154
+ const wildcardDirectories = Array.from(directory.directories.values()).filter((subdirectory) => subdirectory.isWildcard);
152
155
  const wildcardMatches = [];
153
156
  for (const wildcardDirectory of wildcardDirectories) {
154
157
  const wildcardMatch = this.matchWithinDirectory(wildcardDirectory, remainingSegments, {
@@ -171,10 +174,10 @@ export class ModuleTree {
171
174
  get routes() {
172
175
  const routes = [];
173
176
  function traverse(directory, path) {
174
- Object.values(directory.directories).forEach((subdirectory) => {
177
+ directory.directories.forEach((subdirectory) => {
175
178
  traverse(subdirectory, `${path}/${subdirectory.rawName}`);
176
179
  });
177
- Object.values(directory.files).forEach((file) => {
180
+ directory.files.forEach((file) => {
178
181
  const methods = Object.entries(file.module).map(([method, implementation]) => [method, String(implementation)]);
179
182
  routes.push({
180
183
  methods: Object.fromEntries(methods),
@@ -1,5 +1,5 @@
1
1
  import { bundle } from "@apidevtools/json-schema-ref-parser";
2
- import yaml from "js-yaml";
2
+ import { dump } from "js-yaml";
3
3
  export function openapiMiddleware(openApiPath, url) {
4
4
  return async (ctx, next) => {
5
5
  if (ctx.URL.pathname === "/counterfact/openapi") {
@@ -11,7 +11,7 @@ export function openapiMiddleware(openApiPath, url) {
11
11
  });
12
12
  // OpenApi 2 support:
13
13
  openApiDocument.host = url;
14
- ctx.body = yaml.dump(openApiDocument);
14
+ ctx.body = dump(openApiDocument);
15
15
  return;
16
16
  }
17
17
  await next();
@@ -26,10 +26,10 @@ function castParameter(value, type) {
26
26
  }
27
27
  return value;
28
28
  }
29
- function castParameters(parameters = {}, parameterTypes = {}) {
29
+ function castParameters(parameters = {}, parameterTypes = new Map()) {
30
30
  const copy = {};
31
31
  Object.entries(parameters).forEach(([key, value]) => {
32
- copy[key] = castParameter(value, parameterTypes?.[key] ?? "string");
32
+ copy[key] = castParameter(value, parameterTypes.get(key) ?? "string");
33
33
  });
34
34
  return copy;
35
35
  }
@@ -0,0 +1,61 @@
1
+ import Ajv from "ajv";
2
+ const ajv = new Ajv({
3
+ allErrors: true,
4
+ unknownFormats: "ignore",
5
+ coerceTypes: false,
6
+ });
7
+ function findMissingRequired(parameters, location, values) {
8
+ return parameters
9
+ .filter((p) => p.in === location && p.required === true)
10
+ .filter((p) => !(p.name in values) || values[p.name] === undefined)
11
+ .map((p) => `${location} parameter '${p.name}' is required`);
12
+ }
13
+ export function validateRequest(operation, request) {
14
+ if (!operation) {
15
+ return { errors: [], valid: true };
16
+ }
17
+ const errors = [];
18
+ const parameters = operation.parameters ?? [];
19
+ // For query and header parameters, HTTP always delivers values as strings.
20
+ // Only check that required parameters are present; type coercion is handled
21
+ // by the registry before the route handler is called.
22
+ errors.push(...findMissingRequired(parameters, "query", request.query));
23
+ errors.push(...findMissingRequired(parameters, "header", request.headers));
24
+ // Validate request body (OpenAPI 3.x requestBody)
25
+ if (operation.requestBody?.content !== undefined) {
26
+ const schema = operation.requestBody.content["application/json"]?.schema ??
27
+ operation.requestBody.content["application/x-www-form-urlencoded"]
28
+ ?.schema;
29
+ if (schema !== undefined) {
30
+ const valid = ajv.validate(schema, request.body);
31
+ if (!valid && ajv.errors) {
32
+ for (const error of ajv.errors) {
33
+ const path = error.instancePath ??
34
+ error.dataPath ??
35
+ "";
36
+ errors.push(`body${path} ${error.message ?? "is invalid"}`);
37
+ }
38
+ }
39
+ }
40
+ else if (operation.requestBody.required === true && !request.body) {
41
+ errors.push("body is required");
42
+ }
43
+ }
44
+ // Validate request body (OpenAPI 2.x body parameter)
45
+ const bodyParam = parameters.find((p) => p.in === "body");
46
+ if (bodyParam?.schema !== undefined) {
47
+ const valid = ajv.validate(bodyParam.schema, request.body);
48
+ if (!valid && ajv.errors) {
49
+ for (const error of ajv.errors) {
50
+ const path = error.instancePath ??
51
+ error.dataPath ??
52
+ "";
53
+ errors.push(`body${path} ${error.message ?? "is invalid"}`);
54
+ }
55
+ }
56
+ }
57
+ return {
58
+ errors,
59
+ valid: errors.length === 0,
60
+ };
61
+ }
@@ -67,12 +67,20 @@ export class Transpiler extends EventTarget {
67
67
  async transpileFile(eventName, sourcePath, destinationPath) {
68
68
  ensureDirectoryExists(destinationPath);
69
69
  const source = await fs.readFile(sourcePath, "utf8");
70
- const result = ts.transpileModule(source, {
70
+ const transpileOutput = ts.transpileModule(source, {
71
71
  compilerOptions: {
72
72
  module: ts.ModuleKind[this.moduleKind.toLowerCase() === "module" ? "ES2022" : "CommonJS"],
73
73
  target: ts.ScriptTarget.ES2015,
74
74
  },
75
- }).outputText;
75
+ reportDiagnostics: true,
76
+ });
77
+ if (transpileOutput.diagnostics?.length) {
78
+ for (const diagnostic of transpileOutput.diagnostics) {
79
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
80
+ debug("TypeScript diagnostic in %s: %s", sourcePath, message);
81
+ }
82
+ }
83
+ const result = transpileOutput.outputText;
76
84
  const fullDestination = nodePath
77
85
  .join(sourcePath
78
86
  .replace(this.sourcePath, this.destinationPath)
@@ -82,10 +90,10 @@ export class Transpiler extends EventTarget {
82
90
  try {
83
91
  await fs.writeFile(fullDestination, resultWithTransformedFileExtensions);
84
92
  }
85
- catch {
86
- debug("error transpiling %s", fullDestination);
93
+ catch (error) {
94
+ debug("error writing transpiled output to %s: %o", fullDestination, error);
87
95
  this.dispatchEvent(new Event("error"));
88
- throw new Error("could not transpile");
96
+ throw new Error("could not transpile", { cause: error });
89
97
  }
90
98
  this.dispatchEvent(new Event("write"));
91
99
  }
@@ -12,6 +12,9 @@ export class Coder {
12
12
  beforeExport(_path) {
13
13
  return "";
14
14
  }
15
+ jsdoc() {
16
+ return "";
17
+ }
15
18
  write(script) {
16
19
  if (this.requirement.isReference) {
17
20
  return script.import(this);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Builds a JSDoc comment string from OpenAPI schema metadata.
3
+ * Returns an empty string if there is no relevant metadata.
4
+ */
5
+ export function buildJsDoc(data) {
6
+ const lines = [];
7
+ const description = data["description"];
8
+ const summary = data["summary"];
9
+ const example = data["example"];
10
+ const examples = data["examples"];
11
+ const defaultValue = data["default"];
12
+ const format = data["format"];
13
+ const deprecated = data["deprecated"];
14
+ const mainText = description ?? summary;
15
+ if (mainText) {
16
+ // Escape */ to prevent prematurely closing the JSDoc block
17
+ const escaped = String(mainText).replace(/\*\//gu, "* /");
18
+ const textLines = escaped.split("\n");
19
+ for (const line of textLines) {
20
+ lines.push(` * ${line}`);
21
+ }
22
+ }
23
+ if (format !== undefined) {
24
+ lines.push(` * @format ${format}`);
25
+ }
26
+ if (defaultValue !== undefined) {
27
+ lines.push(` * @default ${JSON.stringify(defaultValue)}`);
28
+ }
29
+ // Use scalar `example`, or fall back to the first value from `examples`
30
+ const exampleValue = example !== undefined
31
+ ? example
32
+ : examples !== undefined
33
+ ? Object.values(examples)[0]?.value
34
+ : undefined;
35
+ if (exampleValue !== undefined) {
36
+ lines.push(` * @example ${JSON.stringify(exampleValue)}`);
37
+ }
38
+ if (deprecated === true) {
39
+ lines.push(` * @deprecated`);
40
+ }
41
+ if (lines.length === 0) {
42
+ return "";
43
+ }
44
+ return `/**\n${lines.join("\n")}\n */\n`;
45
+ }
@@ -1,5 +1,6 @@
1
1
  import nodePath from "node:path";
2
2
  import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
3
+ import { buildJsDoc } from "./jsdoc.js";
3
4
  import { ParameterExportTypeCoder } from "./parameter-export-type-coder.js";
4
5
  import { ParametersTypeCoder } from "./parameters-type-coder.js";
5
6
  import { READ_ONLY_COMMENTS } from "./read-only-comments.js";
@@ -88,6 +89,9 @@ export class OperationTypeCoder extends TypeCoder {
88
89
  ? sanitizeIdentifier(operationId)
89
90
  : `HTTP_${this.requestMethod.toUpperCase()}`;
90
91
  }
92
+ jsdoc() {
93
+ return buildJsDoc(this.requirement.data);
94
+ }
91
95
  names() {
92
96
  return super.names(this.getOperationBaseName());
93
97
  }
@@ -1,4 +1,5 @@
1
1
  import nodePath from "node:path";
2
+ import { buildJsDoc } from "./jsdoc.js";
2
3
  import { SchemaTypeCoder } from "./schema-type-coder.js";
3
4
  import { TypeCoder } from "./type-coder.js";
4
5
  export class ParametersTypeCoder extends TypeCoder {
@@ -23,7 +24,10 @@ export class ParametersTypeCoder extends TypeCoder {
23
24
  const schema = parameter.has("schema")
24
25
  ? parameter.get("schema")
25
26
  : parameter;
26
- return `"${name}"${optionalFlag}: ${new SchemaTypeCoder(schema).write(script)}`;
27
+ const comment = buildJsDoc(parameter.data);
28
+ const commentPrefix = comment ? `\n${comment}` : "";
29
+ const typeString = new SchemaTypeCoder(schema).write(script);
30
+ return `${commentPrefix}"${name}"${optionalFlag}: ${typeString}`;
27
31
  });
28
32
  if (typeDefinitions.length === 0) {
29
33
  return "never";
@@ -45,7 +45,8 @@ async function removeEmptyDirectories(dir, rootDir) {
45
45
  try {
46
46
  entries = await fs.readdir(dir, { withFileTypes: true });
47
47
  }
48
- catch {
48
+ catch (error) {
49
+ debug("could not read directory %s: %o", dir, error);
49
50
  return;
50
51
  }
51
52
  for (const entry of entries) {
@@ -1,8 +1,12 @@
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
5
  return super.names(this.requirement.data["$ref"]?.split("/").at(-1));
5
6
  }
7
+ jsdoc() {
8
+ return buildJsDoc(this.requirement.data);
9
+ }
6
10
  additionalPropertiesType(script) {
7
11
  const { additionalProperties, properties } = this.requirement.data;
8
12
  if (!additionalProperties.type) {
@@ -22,7 +26,9 @@ export class SchemaTypeCoder extends TypeCoder {
22
26
  const propertyData = property.data;
23
27
  const isRequired = typedData.required?.includes(name) || propertyData.required === true;
24
28
  const optionalFlag = isRequired ? "" : "?";
25
- 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)}`;
26
32
  });
27
33
  if (typedData.additionalProperties) {
28
34
  properties.push(`[key: string]: ${this.additionalPropertiesType(script)}`);
@@ -50,6 +50,7 @@ export class Script {
50
50
  id: coder.id,
51
51
  isDefault,
52
52
  isType,
53
+ jsdoc: "",
53
54
  typeDeclaration: coder.typeDeclaration(this.exports, this),
54
55
  };
55
56
  exportStatement.promise = coder
@@ -57,6 +58,7 @@ export class Script {
57
58
  .then((availableCoder) => {
58
59
  exportStatement.name = name;
59
60
  exportStatement.code = availableCoder.write(this);
61
+ exportStatement.jsdoc = availableCoder.jsdoc();
60
62
  return availableCoder;
61
63
  })
62
64
  .catch((error) => {
@@ -134,18 +136,18 @@ export class Script {
134
136
  });
135
137
  }
136
138
  exportStatements() {
137
- return Array.from(this.exports.values(), ({ beforeExport, code, isDefault, isType, name, typeDeclaration }) => {
139
+ return Array.from(this.exports.values(), ({ beforeExport, code, isDefault, isType, jsdoc, name, typeDeclaration, }) => {
138
140
  if (typeof code === "object" && code !== null && "raw" in code) {
139
141
  return code.raw;
140
142
  }
141
143
  if (isDefault) {
142
- return `${beforeExport}export default ${code};`;
144
+ return `${jsdoc}${beforeExport}export default ${code};`;
143
145
  }
144
146
  const keyword = isType ? "type" : "const";
145
147
  const typeAnnotation = (typeDeclaration ?? "").length === 0
146
148
  ? ""
147
149
  : `:${typeDeclaration ?? ""}`;
148
- return `${beforeExport}export ${keyword} ${name ?? ""}${typeAnnotation} = ${code};`;
150
+ return `${jsdoc}${beforeExport}export ${keyword} ${name ?? ""}${typeAnnotation} = ${code};`;
149
151
  });
150
152
  }
151
153
  contents() {
@@ -21,6 +21,12 @@ export class Specification {
21
21
  return this.rootRequirement.select(url.slice(2));
22
22
  }
23
23
  async load(urlOrPath) {
24
- 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
+ }
25
31
  }
26
32
  }
@@ -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.1",
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",