counterfact 2.5.1 → 2.7.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 (94) hide show
  1. package/README.md +103 -140
  2. package/bin/README.md +25 -4
  3. package/bin/counterfact.js +208 -24
  4. package/bin/register-ts-loader.mjs +17 -0
  5. package/bin/ts-loader.mjs +31 -0
  6. package/dist/app.js +31 -21
  7. package/dist/counterfact-types/cookie-options.js +1 -0
  8. package/dist/counterfact-types/counterfact-response.js +7 -0
  9. package/dist/counterfact-types/example-names.js +1 -0
  10. package/dist/counterfact-types/example.js +1 -0
  11. package/dist/counterfact-types/generic-response-builder.js +1 -0
  12. package/dist/counterfact-types/http-status-code.js +1 -0
  13. package/dist/counterfact-types/if-has-key.js +1 -0
  14. package/dist/counterfact-types/index.js +0 -1
  15. package/dist/counterfact-types/maybe-promise.js +1 -0
  16. package/dist/counterfact-types/media-type.js +1 -0
  17. package/dist/counterfact-types/omit-all.js +1 -0
  18. package/dist/counterfact-types/omit-value-when-never.js +1 -0
  19. package/dist/counterfact-types/open-api-content.js +1 -0
  20. package/dist/counterfact-types/open-api-operation.js +1 -0
  21. package/dist/counterfact-types/open-api-parameters.js +1 -0
  22. package/dist/counterfact-types/open-api-response.js +1 -0
  23. package/dist/counterfact-types/random-function.js +1 -0
  24. package/dist/counterfact-types/response-builder-factory.js +1 -0
  25. package/dist/counterfact-types/response-builder.js +1 -0
  26. package/dist/counterfact-types/wide-operation-argument.js +1 -0
  27. package/dist/counterfact-types/wide-response-builder.js +1 -0
  28. package/dist/migrate/update-route-types.js +30 -10
  29. package/dist/repl/raw-http-client.js +14 -14
  30. package/dist/repl/repl.js +119 -4
  31. package/dist/repl/route-builder.js +270 -0
  32. package/dist/server/config.js +1 -1
  33. package/dist/server/context-registry.js +44 -4
  34. package/dist/server/counterfact-types/cookie-options.ts +14 -0
  35. package/dist/server/counterfact-types/counterfact-response.ts +15 -0
  36. package/dist/server/counterfact-types/example-names.ts +13 -0
  37. package/dist/server/counterfact-types/example.ts +10 -0
  38. package/dist/server/counterfact-types/generic-response-builder.ts +164 -0
  39. package/dist/server/counterfact-types/http-status-code.ts +62 -0
  40. package/dist/server/counterfact-types/if-has-key.ts +19 -0
  41. package/dist/server/counterfact-types/index.ts +20 -328
  42. package/dist/server/counterfact-types/maybe-promise.ts +6 -0
  43. package/dist/server/counterfact-types/media-type.ts +6 -0
  44. package/dist/server/counterfact-types/omit-all.ts +11 -0
  45. package/dist/server/counterfact-types/omit-value-when-never.ts +11 -0
  46. package/dist/server/counterfact-types/open-api-content.ts +8 -0
  47. package/dist/server/counterfact-types/open-api-operation.ts +36 -0
  48. package/dist/server/counterfact-types/open-api-parameters.ts +16 -0
  49. package/dist/server/counterfact-types/open-api-response.ts +22 -0
  50. package/dist/server/counterfact-types/random-function.ts +9 -0
  51. package/dist/server/counterfact-types/response-builder-factory.ts +16 -0
  52. package/dist/server/counterfact-types/response-builder.ts +31 -0
  53. package/dist/server/counterfact-types/wide-operation-argument.ts +17 -0
  54. package/dist/server/counterfact-types/wide-response-builder.ts +26 -0
  55. package/dist/server/create-koa-app.js +1 -20
  56. package/dist/server/determine-module-kind.js +1 -1
  57. package/dist/server/dispatcher.js +39 -15
  58. package/dist/server/file-discovery.js +34 -0
  59. package/dist/server/json-to-xml.js +1 -1
  60. package/dist/server/koa-middleware.js +7 -1
  61. package/dist/server/load-openapi-document.js +13 -0
  62. package/dist/server/middleware-detector.js +8 -0
  63. package/dist/server/module-dependency-graph.js +4 -1
  64. package/dist/server/module-loader.js +81 -33
  65. package/dist/server/module-tree.js +26 -23
  66. package/dist/server/openapi-middleware.js +2 -2
  67. package/dist/server/openapi-watcher.js +35 -0
  68. package/dist/server/registry.js +2 -2
  69. package/dist/server/request-validator.js +57 -0
  70. package/dist/server/response-builder.js +3 -0
  71. package/dist/server/response-validator.js +58 -0
  72. package/dist/server/scenario-registry.js +29 -0
  73. package/dist/server/tools.js +2 -2
  74. package/dist/server/transpiler.js +13 -5
  75. package/dist/typescript-generator/coder.js +7 -2
  76. package/dist/typescript-generator/generate.js +155 -0
  77. package/dist/typescript-generator/jsdoc.js +45 -0
  78. package/dist/typescript-generator/operation-coder.js +1 -1
  79. package/dist/typescript-generator/operation-type-coder.js +5 -49
  80. package/dist/typescript-generator/parameters-type-coder.js +5 -1
  81. package/dist/typescript-generator/prune.js +2 -1
  82. package/dist/typescript-generator/read-only-comments.js +1 -1
  83. package/dist/typescript-generator/requirement.js +8 -1
  84. package/dist/typescript-generator/reserved-words.js +50 -0
  85. package/dist/typescript-generator/schema-type-coder.js +7 -1
  86. package/dist/typescript-generator/script.js +5 -3
  87. package/dist/typescript-generator/specification.js +7 -1
  88. package/dist/util/load-config-file.js +44 -0
  89. package/dist/util/runtime-can-execute-erasable-ts.js +22 -0
  90. package/package.json +12 -12
  91. package/dist/client/README.md +0 -14
  92. package/dist/client/index.html.hbs +0 -244
  93. package/dist/client/rapi-doc.html.hbs +0 -36
  94. package/dist/server/page-middleware.js +0 -23
@@ -1,61 +1,14 @@
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";
7
+ import { RESERVED_WORDS } from "./reserved-words.js";
6
8
  import { ResponsesTypeCoder } from "./responses-type-coder.js";
7
9
  import { SchemaTypeCoder } from "./schema-type-coder.js";
8
10
  import { TypeCoder } from "./type-coder.js";
9
11
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words
10
- const RESERVED_WORDS = new Set([
11
- "break",
12
- "case",
13
- "catch",
14
- "class",
15
- "const",
16
- "continue",
17
- "debugger",
18
- "default",
19
- "delete",
20
- "do",
21
- "else",
22
- "export",
23
- "extends",
24
- "false",
25
- "finally",
26
- "for",
27
- "function",
28
- "if",
29
- "import",
30
- "in",
31
- "instanceof",
32
- "new",
33
- "null",
34
- "return",
35
- "static",
36
- "super",
37
- "switch",
38
- "this",
39
- "throw",
40
- "true",
41
- "try",
42
- "typeof",
43
- "var",
44
- "void",
45
- "while",
46
- "with",
47
- "yield",
48
- "await",
49
- "enum",
50
- "implements",
51
- "interface",
52
- "let",
53
- "package",
54
- "private",
55
- "protected",
56
- "public",
57
- "type",
58
- ]);
59
12
  function sanitizeIdentifier(value) {
60
13
  // Treat any run of non-identifier characters as a camelCase separator
61
14
  let result = value.replaceAll(/[^\w$]+(?<next>.)/gu, (_, char) => char.toUpperCase());
@@ -88,6 +41,9 @@ export class OperationTypeCoder extends TypeCoder {
88
41
  ? sanitizeIdentifier(operationId)
89
42
  : `HTTP_${this.requestMethod.toUpperCase()}`;
90
43
  }
44
+ jsdoc() {
45
+ return buildJsDoc(this.requirement.data);
46
+ }
91
47
  names() {
92
48
  return super.names(this.getOperationBaseName());
93
49
  }
@@ -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,5 +1,5 @@
1
1
  export const READ_ONLY_COMMENTS = [
2
2
  "This code was automatically generated from an OpenAPI description.",
3
3
  "Do not edit this file. Edit the OpenAPI file instead.",
4
- "For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq-generated-code.md",
4
+ "For more information, see https://github.com/pmcelhaney/counterfact/blob/main/docs/faq.md",
5
5
  ];
@@ -36,7 +36,14 @@ export class Requirement {
36
36
  // Unescape URL encoded characters (e.g. %20 -> " ")
37
37
  // Technically we should not be unescaping, but it came up in https://github.com/pmcelhaney/counterfact/issues/1083
38
38
  // and I can't think of a reason anyone would intentionally put a % in a key name.
39
- .map(unescape);
39
+ .map((part) => {
40
+ try {
41
+ return decodeURIComponent(part);
42
+ }
43
+ catch {
44
+ return part;
45
+ }
46
+ });
40
47
  // eslint-disable-next-line @typescript-eslint/no-this-alias
41
48
  let result = this;
42
49
  for (const part of parts) {
@@ -0,0 +1,50 @@
1
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words
2
+ export const RESERVED_WORDS = new Set([
3
+ "break",
4
+ "case",
5
+ "catch",
6
+ "class",
7
+ "const",
8
+ "continue",
9
+ "debugger",
10
+ "default",
11
+ "delete",
12
+ "do",
13
+ "else",
14
+ "export",
15
+ "extends",
16
+ "false",
17
+ "finally",
18
+ "for",
19
+ "function",
20
+ "if",
21
+ "import",
22
+ "in",
23
+ "instanceof",
24
+ "new",
25
+ "null",
26
+ "return",
27
+ "static",
28
+ "super",
29
+ "switch",
30
+ "this",
31
+ "throw",
32
+ "true",
33
+ "try",
34
+ "typeof",
35
+ "var",
36
+ "void",
37
+ "while",
38
+ "with",
39
+ "yield",
40
+ "await",
41
+ "enum",
42
+ "implements",
43
+ "interface",
44
+ "let",
45
+ "package",
46
+ "private",
47
+ "protected",
48
+ "public",
49
+ "type",
50
+ ]);
@@ -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,44 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { load as loadYaml } from "js-yaml";
3
+ function kebabToCamel(str) {
4
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
5
+ }
6
+ function normalizeKeys(obj) {
7
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [kebabToCamel(key), value]));
8
+ }
9
+ /**
10
+ * Loads and parses a counterfact YAML config file.
11
+ *
12
+ * @param configPath - Absolute or relative path to the config file.
13
+ * @param required - When true, throws if the file does not exist.
14
+ * When false (default), returns an empty object for missing files.
15
+ * @returns A plain object of config keys (camelCase) to values.
16
+ */
17
+ export async function loadConfigFile(configPath, required = false) {
18
+ let content;
19
+ try {
20
+ content = await readFile(configPath, "utf8");
21
+ }
22
+ catch (error) {
23
+ if (typeof error === "object" &&
24
+ error !== null &&
25
+ "code" in error &&
26
+ error.code === "ENOENT") {
27
+ if (required) {
28
+ throw new Error(`Config file not found: ${configPath}`, {
29
+ cause: error,
30
+ });
31
+ }
32
+ return {};
33
+ }
34
+ throw error;
35
+ }
36
+ const parsed = loadYaml(content);
37
+ if (parsed === null || parsed === undefined) {
38
+ return {};
39
+ }
40
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
41
+ throw new Error(`Config file must be a YAML object (mapping): ${configPath}`);
42
+ }
43
+ return normalizeKeys(parsed);
44
+ }
@@ -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.7.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",
@@ -21,7 +21,7 @@
21
21
  "license": "MIT",
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "https://github.com/pmcelhaney/counterfact.git"
24
+ "url": "git+https://github.com/pmcelhaney/counterfact.git"
25
25
  },
26
26
  "bugs": "https://github.com/pmcelhaney/counterfact/issues",
27
27
  "homepage": "https://counterfact.dev",
@@ -56,7 +56,7 @@
56
56
  "node": ">=22"
57
57
  },
58
58
  "bin": {
59
- "counterfact": "./bin/counterfact.js"
59
+ "counterfact": "bin/counterfact.js"
60
60
  },
61
61
  "files": [
62
62
  "bin",
@@ -67,7 +67,7 @@
67
67
  "test": "yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest --testPathIgnorePatterns=black-box",
68
68
  "test:black-box": "rimraf dist && yarn build && python3 -m pytest test-black-box/ -v",
69
69
  "test:tsd": "tsd --typings ./dist/server/counterfact-types/index.ts --files ./test/**/*.test-d.ts",
70
- "build": "rm -rf dist && tsc && copyfiles -f \"src/client/**\" dist/client && copyfiles -f \"src/counterfact-types/*.ts\" dist/server/counterfact-types && copyfiles -f \"src/server/*.cjs\" dist/server",
70
+ "build": "rm -rf dist && tsc && copyfiles -f \"src/counterfact-types/*.ts\" dist/server/counterfact-types && copyfiles -f \"src/server/*.cjs\" dist/server",
71
71
  "prepack": "yarn build",
72
72
  "release": "npx changeset publish",
73
73
  "prepare": "husky install",
@@ -84,7 +84,7 @@
84
84
  "@changesets/cli": "2.30.0",
85
85
  "@eslint/js": "10.0.1",
86
86
  "@jest/globals": "^30.3.0",
87
- "@swc/core": "1.15.21",
87
+ "@swc/core": "1.15.24",
88
88
  "@swc/jest": "0.2.39",
89
89
  "@testing-library/dom": "10.4.1",
90
90
  "@types/debug": "^4.1.12",
@@ -94,12 +94,11 @@
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
- "eslint": "10.1.0",
101
+ "eslint": "10.2.0",
103
102
  "eslint-formatter-github-annotations": "0.1.0",
104
103
  "eslint-import-resolver-typescript": "4.4.4",
105
104
  "eslint-plugin-etc": "2.0.3",
@@ -127,12 +126,11 @@
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": "8.18.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
- "handlebars": "4.7.9",
136
134
  "http-terminator": "3.2.0",
137
135
  "js-yaml": "4.1.1",
138
136
  "json-schema-faker": "0.6.0",
@@ -141,12 +139,14 @@
141
139
  "koa-bodyparser": "4.4.1",
142
140
  "koa-proxies": "0.12.4",
143
141
  "koa2-swagger-ui": "5.12.0",
144
- "lodash": "4.18.1",
145
142
  "node-fetch": "3.3.2",
146
143
  "open": "11.0.0",
147
144
  "patch-package": "8.0.1",
145
+ "posthog-node": "^5.28.11",
148
146
  "precinct": "12.2.0",
149
147
  "prettier": "3.8.1",
148
+ "recast": "0.23.11",
149
+ "tsx": "^4.20.3",
150
150
  "typescript": "6.0.2"
151
151
  },
152
152
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
@@ -1,14 +0,0 @@
1
- # `src/client/` — Built-in UI Templates
2
-
3
- This directory contains [Handlebars](https://handlebarsjs.com/) (`.hbs`) templates that are rendered by `page-middleware.ts` to produce the browser-facing pages bundled with Counterfact.
4
-
5
- ## Files
6
-
7
- | File | Description |
8
- |---|---|
9
- | `index.html.hbs` | Template for the Counterfact dashboard (`/counterfact/`); lists registered routes and shows server status |
10
- | `rapi-doc.html.hbs` | Template for the interactive API documentation page (`/counterfact/swagger/`); embeds the [RapiDoc](https://rapidocweb.com/) viewer and adds VSCode "open file" links |
11
-
12
- ## How It Works
13
-
14
- When a request arrives for `/counterfact/` or `/counterfact/swagger/`, `page-middleware.ts` compiles the appropriate template with runtime data (routes, port, base path, etc.) and sends the resulting HTML to the browser. No build step is required; templates are rendered on the fly.