counterfact 0.28.0 → 0.30.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 (41) hide show
  1. package/bin/counterfact.js +17 -22
  2. package/dist/server/app.js +76 -0
  3. package/dist/server/code-generator.js +23 -0
  4. package/dist/server/create-koa-app.js +49 -0
  5. package/dist/{src/server → server}/dispatcher.js +1 -2
  6. package/dist/{src/server → server}/koa-middleware.js +10 -2
  7. package/dist/server/openapi-middleware.js +22 -0
  8. package/dist/server/page-middleware.js +24 -0
  9. package/dist/{src/server → server}/repl.js +1 -0
  10. package/dist/{src/server → server}/response-builder.js +2 -1
  11. package/dist/{src/server → server}/transpiler.js +0 -1
  12. package/dist/{src/typescript-generator → typescript-generator}/generate.js +11 -2
  13. package/dist/{src/typescript-generator → typescript-generator}/repository.js +3 -8
  14. package/dist/{src/typescript-generator → typescript-generator}/response-type-coder.js +5 -9
  15. package/package.json +17 -17
  16. package/dist/src/server/counterfact.js +0 -47
  17. package/dist/src/server/start.js +0 -106
  18. package/dist/templates/response-builder-factory.ts +0 -133
  19. /package/dist/{src/client → client}/index.html.hbs +0 -0
  20. /package/dist/{src/client → client}/rapi-doc.html.hbs +0 -0
  21. /package/dist/{src/migrations → migrations}/0.27.js +0 -0
  22. /package/dist/{templates/response-builder-factory.js → server/config.js} +0 -0
  23. /package/dist/{src/server → server}/constants.js +0 -0
  24. /package/dist/{src/server → server}/context-registry.js +0 -0
  25. /package/dist/{src/server → server}/module-loader.js +0 -0
  26. /package/dist/{src/server → server}/registry.js +0 -0
  27. /package/dist/{src/server → server}/tools.js +0 -0
  28. /package/dist/{src/typescript-generator → typescript-generator}/coder.js +0 -0
  29. /package/dist/{src/typescript-generator → typescript-generator}/context-coder.js +0 -0
  30. /package/dist/{src/typescript-generator → typescript-generator}/context-type-coder.js +0 -0
  31. /package/dist/{src/typescript-generator → typescript-generator}/operation-coder.js +0 -0
  32. /package/dist/{src/typescript-generator → typescript-generator}/operation-type-coder.js +0 -0
  33. /package/dist/{src/typescript-generator → typescript-generator}/parameters-type-coder.js +0 -0
  34. /package/dist/{src/typescript-generator → typescript-generator}/printers.js +0 -0
  35. /package/dist/{src/typescript-generator → typescript-generator}/requirement.js +0 -0
  36. /package/dist/{src/typescript-generator → typescript-generator}/schema-coder.js +0 -0
  37. /package/dist/{src/typescript-generator → typescript-generator}/schema-type-coder.js +0 -0
  38. /package/dist/{src/typescript-generator → typescript-generator}/script.js +0 -0
  39. /package/dist/{src/typescript-generator → typescript-generator}/specification.js +0 -0
  40. /package/dist/{src/util → util}/ensure-directory-exists.js +0 -0
  41. /package/dist/{src/util → util}/read-file.js +0 -0
@@ -6,10 +6,8 @@ import { program } from "commander";
6
6
  import createDebug from "debug";
7
7
  import open from "open";
8
8
 
9
- import { migrate } from "../dist/src/migrations/0.27.js";
10
- import { startRepl } from "../dist/src/server/repl.js";
11
- import { start } from "../dist/src/server/start.js";
12
- import { generate } from "../dist/src/typescript-generator/generate.js";
9
+ import { migrate } from "../dist/migrations/0.27.js";
10
+ import { counterfact } from "../dist/server/app.js";
13
11
 
14
12
  const DEFAULT_PORT = 3100;
15
13
 
@@ -23,10 +21,6 @@ async function main(source, destination) {
23
21
 
24
22
  const options = program.opts();
25
23
 
26
- debug("options: %o", options);
27
- debug("source: %s", source);
28
- debug("destination: %s", destination);
29
-
30
24
  const destinationPath = nodePath
31
25
  .join(process.cwd(), destination)
32
26
  .replaceAll("\\", "/");
@@ -35,14 +29,12 @@ async function main(source, destination) {
35
29
  migrate(destinationPath);
36
30
  debug("done with migration");
37
31
 
38
- debug('generating code at "%s"', destinationPath);
39
-
40
- await generate(source, destinationPath);
41
-
42
- debug("generated code", destinationPath);
43
-
44
32
  const basePath = nodePath.resolve(destinationPath).replaceAll("\\", "/");
45
33
 
34
+ debug("options: %o", options);
35
+ debug("source: %s", source);
36
+ debug("destination: %s", destination);
37
+
46
38
  const openBrowser = options.open;
47
39
 
48
40
  const url = `http://localhost:${options.port}`;
@@ -56,13 +48,14 @@ async function main(source, destination) {
56
48
  port: options.port,
57
49
  proxyEnabled: Boolean(options.proxyUrl),
58
50
  proxyUrl: options.proxyUrl,
51
+ routePrefix: options.prefix,
59
52
  };
60
53
 
61
- debug("starting server (%o)", config);
54
+ debug("loading counterfact (%o)", config);
62
55
 
63
- const { contextRegistry } = await start(config);
56
+ const { start } = await counterfact(config);
64
57
 
65
- debug("started server");
58
+ debug("loaded counterfact", config);
66
59
 
67
60
  const waysToInteract = [
68
61
  `Call the REST APIs at ${url} (with your front end app, curl, Postman, etc.)`,
@@ -90,13 +83,11 @@ async function main(source, destination) {
90
83
 
91
84
  process.stdout.write("\n\n");
92
85
 
93
- process.stdout.write("Starting REPL...\n");
86
+ debug("starting server");
94
87
 
95
- debug("starting repl");
88
+ await start();
96
89
 
97
- startRepl(contextRegistry, config);
98
-
99
- debug("started repl");
90
+ debug("started server");
100
91
 
101
92
  if (openBrowser) {
102
93
  debug("opening browser");
@@ -116,6 +107,10 @@ program
116
107
  .option("--swagger", "include swagger-ui")
117
108
  .option("--open", "open a browser")
118
109
  .option("--proxy-url <string>", "proxy URL")
110
+ .option(
111
+ "--prefix <string>",
112
+ "base path from which routes will be served (e.g. /api/v1)",
113
+ )
119
114
  .action(main)
120
115
  // eslint-disable-next-line sonar/process-argv
121
116
  .parse(process.argv);
@@ -0,0 +1,76 @@
1
+ import nodePath from "node:path";
2
+ import { createHttpTerminator } from "http-terminator";
3
+ import yaml from "js-yaml";
4
+ import $RefParser from "json-schema-ref-parser";
5
+ import { readFile } from "../util/read-file.js";
6
+ import { CodeGenerator } from "./code-generator.js";
7
+ import { ContextRegistry } from "./context-registry.js";
8
+ import { createKoaApp } from "./create-koa-app.js";
9
+ import { Dispatcher } from "./dispatcher.js";
10
+ import { koaMiddleware } from "./koa-middleware.js";
11
+ import { ModuleLoader } from "./module-loader.js";
12
+ import { Registry } from "./registry.js";
13
+ import { startRepl } from "./repl.js";
14
+ import { Transpiler } from "./transpiler.js";
15
+ async function loadOpenApiDocument(source) {
16
+ try {
17
+ const text = await readFile(source);
18
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
19
+ const openApiDocument = (await yaml.load(text));
20
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
21
+ return (await $RefParser.dereference(openApiDocument));
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ // eslint-disable-next-line max-statements
28
+ export async function counterfact(config) {
29
+ const modulesPath = config.basePath;
30
+ const compiledPathsDirectory = nodePath
31
+ .join(modulesPath, ".cache")
32
+ .replaceAll("\\", "/");
33
+ const registry = new Registry();
34
+ const contextRegistry = new ContextRegistry();
35
+ const codeGenerator = new CodeGenerator(config.openApiPath, config.basePath);
36
+ const dispatcher = new Dispatcher(registry, contextRegistry, await loadOpenApiDocument(config.openApiPath));
37
+ const transpiler = new Transpiler(nodePath.join(modulesPath, "paths").replaceAll("\\", "/"), compiledPathsDirectory);
38
+ const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
39
+ const middleware = koaMiddleware(dispatcher, config);
40
+ const koaApp = createKoaApp(registry, middleware, config);
41
+ // eslint-disable-next-line max-statements
42
+ async function start(options = {}) {
43
+ const http = options.http ?? true;
44
+ await codeGenerator.watch();
45
+ await transpiler.watch();
46
+ await moduleLoader.load();
47
+ await moduleLoader.watch();
48
+ // eslint-disable-next-line @typescript-eslint/init-declarations
49
+ let httpTerminator;
50
+ if (http) {
51
+ const server = koaApp.listen({
52
+ port: config.port,
53
+ });
54
+ httpTerminator = createHttpTerminator({
55
+ server,
56
+ });
57
+ }
58
+ const replServer = startRepl(contextRegistry, config);
59
+ return {
60
+ replServer,
61
+ async stop() {
62
+ await codeGenerator.stopWatching();
63
+ await transpiler.stopWatching();
64
+ await moduleLoader.stopWatching();
65
+ await httpTerminator?.terminate();
66
+ },
67
+ };
68
+ }
69
+ return {
70
+ contextRegistry,
71
+ koaApp,
72
+ koaMiddleware: middleware,
73
+ registry,
74
+ start,
75
+ };
76
+ }
@@ -0,0 +1,23 @@
1
+ import { watch } from "chokidar";
2
+ import { generate } from "../typescript-generator/generate.js";
3
+ export class CodeGenerator {
4
+ openapiPath;
5
+ destination;
6
+ watcher;
7
+ constructor(openApiPath, destination) {
8
+ this.openapiPath = openApiPath;
9
+ this.destination = destination;
10
+ }
11
+ async watch() {
12
+ await generate(this.openapiPath, this.destination);
13
+ if (this.openapiPath.startsWith("http")) {
14
+ return;
15
+ }
16
+ this.watcher = watch(this.openapiPath).on("change", () => {
17
+ void generate(this.openapiPath, this.destination);
18
+ });
19
+ }
20
+ async stopWatching() {
21
+ await this.watcher?.close();
22
+ }
23
+ }
@@ -0,0 +1,49 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import createDebug from "debug";
3
+ import Koa from "koa";
4
+ import bodyParser from "koa-bodyparser";
5
+ import { koaSwagger } from "koa2-swagger-ui";
6
+ import { openapiMiddleware } from "./openapi-middleware.js";
7
+ import { pageMiddleware } from "./page-middleware.js";
8
+ const debug = createDebug("counterfact:server:create-koa-app");
9
+ // eslint-disable-next-line max-statements
10
+ export function createKoaApp(registry, koaMiddleware, config) {
11
+ const app = new Koa();
12
+ app.use(openapiMiddleware(config.openApiPath, `//localhost:${config.port}`));
13
+ app.use(koaSwagger({
14
+ routePrefix: "/counterfact/swagger",
15
+ swaggerOptions: {
16
+ url: "/counterfact/openapi",
17
+ },
18
+ }));
19
+ debug("basePath: %s", config.basePath);
20
+ debug("routes", registry.routes);
21
+ app.use(pageMiddleware("/counterfact/", "index", {
22
+ basePath: config.basePath,
23
+ methods: ["get", "post", "put", "delete", "patch"],
24
+ openApiHref: config.openApiPath.includes("://")
25
+ ? config.openApiPath
26
+ : pathToFileURL(config.openApiPath).href,
27
+ openApiPath: config.openApiPath,
28
+ get routes() {
29
+ return registry.routes;
30
+ },
31
+ }));
32
+ app.use(async (ctx, next) => {
33
+ if (ctx.URL.pathname === "/counterfact") {
34
+ ctx.redirect("/counterfact/");
35
+ return;
36
+ }
37
+ // eslint-disable-next-line n/callback-return
38
+ await next();
39
+ });
40
+ app.use(pageMiddleware("/counterfact/rapidoc", "rapi-doc", {
41
+ basePath: config.basePath,
42
+ get routes() {
43
+ return registry.routes;
44
+ },
45
+ }));
46
+ app.use(bodyParser());
47
+ app.use(koaMiddleware);
48
+ return app;
49
+ }
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable import/newline-after-import */
2
- /* eslint-disable max-lines */
3
2
  import { mediaTypes } from "@hapi/accept";
4
3
  import createDebugger from "debug";
5
4
  // eslint-disable-next-line @typescript-eslint/no-shadow
6
5
  import fetch, { Headers } from "node-fetch";
7
- import { createResponseBuilder, } from "./response-builder.js";
6
+ import { createResponseBuilder } from "./response-builder.js";
8
7
  import { Tools } from "./tools.js";
9
8
  const debug = createDebugger("counterfact:server:dispatcher");
10
9
  export class Dispatcher {
@@ -8,13 +8,20 @@ function addCors(ctx, headers) {
8
8
  ctx.set("Access-Control-Expose-Headers", headers?.["access-control-request-headers"] ?? []);
9
9
  ctx.set("Access-Control-Allow-Credentials", "true");
10
10
  }
11
- export function koaMiddleware(dispatcher, { proxyEnabled = false, proxyUrl = "" } = {}, proxy = koaProxy) {
11
+ export function koaMiddleware(dispatcher, { proxyEnabled = false, proxyUrl = "", routePrefix = "" } = {}, proxy = koaProxy) {
12
12
  // eslint-disable-next-line max-statements
13
13
  return async function middleware(ctx, next) {
14
- const { body, headers, path, query } = ctx.request;
14
+ if (!ctx.request.path.startsWith(routePrefix)) {
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
16
+ return await next();
17
+ }
18
+ /* @ts-expect-error the body comes from koa-bodyparser, not sure how to fix this */
19
+ const { body, headers, query } = ctx.request;
20
+ const path = ctx.request.path.slice(routePrefix.length);
15
21
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
16
22
  const method = ctx.request.method;
17
23
  if (proxyEnabled && proxyUrl) {
24
+ /* @ts-expect-error the body comes from koa-bodyparser, not sure how to fix this */
18
25
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
19
26
  return proxy({ host: proxyUrl })(ctx, next);
20
27
  }
@@ -24,6 +31,7 @@ export function koaMiddleware(dispatcher, { proxyEnabled = false, proxyUrl = ""
24
31
  return undefined;
25
32
  }
26
33
  const response = await dispatcher.request({
34
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
27
35
  body,
28
36
  /* @ts-expect-error the value of a header can be an array and we don't have a solution for that yet */
29
37
  headers,
@@ -0,0 +1,22 @@
1
+ import yaml from "js-yaml";
2
+ import { readFile } from "../util/read-file.js";
3
+ export function openapiMiddleware(openApiPath, url) {
4
+ return async (ctx, next) => {
5
+ if (ctx.URL.pathname === "/counterfact/openapi") {
6
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
7
+ const openApiDocument = (await yaml.load(await readFile(openApiPath)));
8
+ openApiDocument.servers ??= [];
9
+ openApiDocument.servers.unshift({
10
+ description: "Counterfact",
11
+ url,
12
+ });
13
+ // OpenApi 2 support:
14
+ openApiDocument.host = url;
15
+ // eslint-disable-next-line require-atomic-updates
16
+ ctx.body = yaml.dump(openApiDocument);
17
+ return;
18
+ }
19
+ // eslint-disable-next-line n/callback-return
20
+ await next();
21
+ };
22
+ }
@@ -0,0 +1,24 @@
1
+ import nodePath from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import createDebug from "debug";
4
+ import Handlebars from "handlebars";
5
+ import { readFile } from "../util/read-file.js";
6
+ // eslint-disable-next-line no-underscore-dangle
7
+ const __dirname = nodePath.dirname(fileURLToPath(import.meta.url));
8
+ const debug = createDebug("counterfact:server:page-middleware");
9
+ Handlebars.registerHelper("escape_route", (route) => route.replaceAll(/[^\w/]/gu, "-"));
10
+ export function pageMiddleware(pathname, templateName, locals) {
11
+ return async (ctx, next) => {
12
+ const pathToHandlebarsTemplate = nodePath
13
+ .join(__dirname, `../client/${templateName}.html.hbs`)
14
+ .replaceAll("\\", "/");
15
+ debug("pathToHandlebarsTemplate: %s", pathToHandlebarsTemplate);
16
+ const render = Handlebars.compile(await readFile(pathToHandlebarsTemplate));
17
+ if (ctx.URL.pathname === pathname) {
18
+ ctx.body = render(locals);
19
+ return;
20
+ }
21
+ // eslint-disable-next-line n/callback-return
22
+ await next();
23
+ };
24
+ }
@@ -30,4 +30,5 @@ export function startRepl(contextRegistry, config) {
30
30
  replServer.context.loadContext = (path) => contextRegistry.find(path);
31
31
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
32
32
  replServer.context.context = replServer.context.loadContext("/");
33
+ return replServer;
33
34
  }
@@ -81,7 +81,8 @@ export function createResponseBuilder(operation) {
81
81
  }
82
82
  const body = response.examples
83
83
  ? oneOf(response.examples)
84
- : JSONSchemaFaker.generate(response.schema ?? { type: "object" });
84
+ : // eslint-disable-next-line total-functions/no-unsafe-readonly-mutable-assignment
85
+ JSONSchemaFaker.generate(response.schema ?? { type: "object" });
85
86
  return {
86
87
  ...this,
87
88
  content: operation.produces?.map((type) => ({
@@ -71,7 +71,6 @@ export class Transpiler extends EventTarget {
71
71
  .replace(".ts", ".mjs"))
72
72
  .replaceAll("\\", "/");
73
73
  try {
74
- // eslint-disable-next-line total-functions/no-unsafe-readonly-mutable-assignment
75
74
  await fs.writeFile(fullDestination, result);
76
75
  }
77
76
  catch {
@@ -22,6 +22,15 @@ async function buildCacheDirectory(destination) {
22
22
  await fs.writeFile(cacheReadmePath, "This directory contains compiled JS files from the paths directory. Do not edit these files directly.\n", "utf8");
23
23
  }
24
24
  }
25
+ async function getPathsFromSpecification(specification) {
26
+ try {
27
+ return await specification.requirementAt("#/paths");
28
+ }
29
+ catch (error) {
30
+ process.stderr.write(`Could not find #/paths in the specification.\n${error}\n`);
31
+ return [];
32
+ }
33
+ }
25
34
  // eslint-disable-next-line max-statements
26
35
  export async function generate(source, destination, repository = new Repository()) {
27
36
  debug("generating code from %s to %s", source, destination);
@@ -31,8 +40,8 @@ export async function generate(source, destination, repository = new Repository(
31
40
  debug("creating specification from %s", source);
32
41
  const specification = new Specification(source);
33
42
  debug("created specification: $o", specification);
34
- debug("getting reading the #/paths from the specification");
35
- const paths = await specification.requirementAt("#/paths");
43
+ debug("reading the #/paths from the specification");
44
+ const paths = await getPathsFromSpecification(specification);
36
45
  debug("got %i paths", paths.size);
37
46
  paths.forEach((pathDefinition, key) => {
38
47
  debug("processing path %s", key);
@@ -31,14 +31,9 @@ export class Repository {
31
31
  }
32
32
  }
33
33
  copyCoreFiles(destination) {
34
- const files = ["response-builder-factory.ts"];
35
- return files.map((file) => {
36
- const path = nodePath.join(destination, file).replaceAll("\\", "/");
37
- process.stdout.write(`writing ${path}\n`);
38
- return fs.copyFile(nodePath
39
- .join(__dirname, `../../templates/${file}`)
40
- .replaceAll("\\", "/"), path);
41
- });
34
+ return fs.copyFile(nodePath
35
+ .join(__dirname, "../../src/server/types.d.ts")
36
+ .replaceAll("\\", "/"), nodePath.join(destination, "types.d.ts").replaceAll("\\", "/"));
42
37
  }
43
38
  async writeFiles(destination) {
44
39
  debug("waiting for %i or more scripts to finish before writing files", this.scripts.size);
@@ -8,7 +8,6 @@ export class ResponseTypeCoder extends Coder {
8
8
  this.openApi2MediaTypes = openApi2MediaTypes;
9
9
  }
10
10
  typeForDefaultStatusCode(listedStatusCodes) {
11
- this.needsHttpStatusCodeImport = true;
12
11
  const definedStatusCodes = listedStatusCodes.filter((key) => key !== "default");
13
12
  if (definedStatusCodes.length === 0) {
14
13
  return "[statusCode in HttpStatusCode]";
@@ -72,14 +71,11 @@ export class ResponseTypeCoder extends Coder {
72
71
  .slice(0, -1)
73
72
  .map(() => "..")
74
73
  .join("/");
75
- script.importExternalType("ResponseBuilderFactory", nodePath
76
- .join(basePath, "response-builder-factory.js")
77
- .replaceAll("\\", "/"));
78
- if (this.needsHttpStatusCodeImport) {
79
- script.importExternalType("HttpStatusCode", nodePath
80
- .join(basePath, "response-builder-factory.js")
81
- .replaceAll("\\", "/"));
74
+ script.importExternalType("ResponseBuilderFactory", nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"));
75
+ const text = `ResponseBuilderFactory<${this.buildResponseObjectType(script)}>`;
76
+ if (text.includes("HttpStatusCode")) {
77
+ script.importExternalType("HttpStatusCode", nodePath.join(basePath, "types.d.ts").replaceAll("\\", "/"));
82
78
  }
83
- return `ResponseBuilderFactory<${this.buildResponseObjectType(script)}>`;
79
+ return text;
84
80
  }
85
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "counterfact",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "a library for building a fake REST API for testing",
5
5
  "type": "module",
6
6
  "main": "./src/server/counterfact.js",
@@ -31,7 +31,7 @@
31
31
  "test": "yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest --testPathIgnorePatterns=black-box --forceExit",
32
32
  "test:black-box": "rimraf dist && rimraf out && yarn build && yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest black-box --forceExit --coverage=false",
33
33
  "test:mutants": "stryker run stryker.config.json",
34
- "build": "tsc && copyfiles src/client/** dist && copyfiles templates/** dist",
34
+ "build": "tsc && copyfiles -f \"src/client/**\" dist/client",
35
35
  "prepack": "yarn build",
36
36
  "release": "npx changeset publish",
37
37
  "prepare": "husky install",
@@ -44,32 +44,32 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@changesets/cli": "2.26.2",
47
- "@stryker-mutator/core": "7.2.0",
48
- "@stryker-mutator/jest-runner": "7.2.0",
49
- "@stryker-mutator/typescript-checker": "7.2.0",
50
- "@swc/core": "1.3.93",
47
+ "@stryker-mutator/core": "7.3.0",
48
+ "@stryker-mutator/jest-runner": "7.3.0",
49
+ "@stryker-mutator/typescript-checker": "7.3.0",
50
+ "@swc/core": "1.3.96",
51
51
  "@swc/jest": "0.2.29",
52
52
  "@testing-library/dom": "9.3.3",
53
- "@types/jest": "29.5.5",
54
- "@types/js-yaml": "4.0.7",
55
- "@types/koa": "2.13.8",
56
- "@types/koa-bodyparser": "4.3.10",
57
- "@types/koa-proxy": "1.0.5",
58
- "@types/koa-static": "4.0.2",
53
+ "@types/jest": "29.5.8",
54
+ "@types/js-yaml": "4.0.9",
55
+ "@types/koa": "2.13.11",
56
+ "@types/koa-bodyparser": "4.3.12",
57
+ "@types/koa-proxy": "1.0.7",
58
+ "@types/koa-static": "4.0.4",
59
59
  "copyfiles": "2.4.1",
60
- "eslint": "8.51.0",
60
+ "eslint": "8.53.0",
61
61
  "eslint-config-hardcore": "41.3.0",
62
62
  "eslint-formatter-github-annotations": "0.1.0",
63
63
  "eslint-import-resolver-typescript": "3.6.1",
64
64
  "eslint-plugin-etc": "2.0.3",
65
65
  "eslint-plugin-file-progress": "1.3.0",
66
- "eslint-plugin-import": "2.28.1",
67
- "eslint-plugin-jest": "27.4.2",
66
+ "eslint-plugin-import": "2.29.0",
67
+ "eslint-plugin-jest": "27.6.0",
68
68
  "eslint-plugin-jest-dom": "5.1.0",
69
69
  "eslint-plugin-no-explicit-type-exports": "0.12.1",
70
70
  "eslint-plugin-unused-imports": "3.0.0",
71
71
  "husky": "8.0.3",
72
- "jest": "28.1.3",
72
+ "jest": "29.7.0",
73
73
  "node-mocks-http": "1.13.0",
74
74
  "nodemon": "3.0.1",
75
75
  "patch-package": "8.0.0",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "@hapi/accept": "6.0.3",
82
- "@types/json-schema": "7.0.13",
82
+ "@types/json-schema": "7.0.15",
83
83
  "chokidar": "3.5.3",
84
84
  "commander": "11.1.0",
85
85
  "debug": "4.3.4",
@@ -1,47 +0,0 @@
1
- import nodePath from "node:path";
2
- import yaml from "js-yaml";
3
- import $RefParser from "json-schema-ref-parser";
4
- import { readFile } from "../util/read-file.js";
5
- import { ContextRegistry } from "./context-registry.js";
6
- import { Dispatcher } from "./dispatcher.js";
7
- import { koaMiddleware } from "./koa-middleware.js";
8
- import { ModuleLoader } from "./module-loader.js";
9
- import { Registry } from "./registry.js";
10
- import { Transpiler } from "./transpiler.js";
11
- async function loadOpenApiDocument(source) {
12
- try {
13
- const text = await readFile(source);
14
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
15
- const openApiDocument = (await yaml.load(text));
16
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
17
- return (await $RefParser.dereference(openApiDocument));
18
- }
19
- catch {
20
- return undefined;
21
- }
22
- }
23
- // eslint-disable-next-line max-statements
24
- export async function counterfact(basePath, openApiPath = nodePath
25
- .join(basePath, "../openapi.yaml")
26
- .replaceAll("\\", "/"), options = {}) {
27
- const openApiDocument = await loadOpenApiDocument(openApiPath);
28
- const registry = new Registry();
29
- const modulesPath = basePath;
30
- const contextRegistry = new ContextRegistry();
31
- const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument);
32
- const compiledPathsDirectory = nodePath
33
- .join(modulesPath, ".cache")
34
- .replaceAll("\\", "/");
35
- const transpiler = new Transpiler(nodePath.join(modulesPath, "paths").replaceAll("\\", "/"), compiledPathsDirectory);
36
- await transpiler.watch();
37
- const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
38
- await moduleLoader.load();
39
- await moduleLoader.watch();
40
- return {
41
- contextRegistry,
42
- // eslint-disable-next-line total-functions/no-unsafe-readonly-mutable-assignment
43
- koaMiddleware: koaMiddleware(dispatcher, options),
44
- moduleLoader,
45
- registry,
46
- };
47
- }
@@ -1,106 +0,0 @@
1
- /* eslint-disable max-statements */
2
- import nodePath, { dirname } from "node:path";
3
- import { fileURLToPath, pathToFileURL } from "node:url";
4
- import createDebug from "debug";
5
- import Handlebars from "handlebars";
6
- import { createHttpTerminator } from "http-terminator";
7
- import yaml from "js-yaml";
8
- import Koa from "koa";
9
- import bodyParser from "koa-bodyparser";
10
- import { koaSwagger } from "koa2-swagger-ui";
11
- import { readFile } from "../util/read-file.js";
12
- import { counterfact } from "./counterfact.js";
13
- const debug = createDebug("counterfact:server:start");
14
- // eslint-disable-next-line @typescript-eslint/init-declarations
15
- let httpTerminator;
16
- // eslint-disable-next-line no-underscore-dangle
17
- const __dirname = dirname(fileURLToPath(import.meta.url));
18
- const DEFAULT_PORT = 3100;
19
- Handlebars.registerHelper("escape_route", (route) => route.replaceAll(/[^\w/]/gu, "-"));
20
- function openapi(openApiPath, url) {
21
- return async (ctx, next) => {
22
- if (ctx.URL.pathname === "/counterfact/openapi") {
23
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
24
- const openApiDocument = (await yaml.load(await readFile(openApiPath)));
25
- openApiDocument.servers ??= [];
26
- openApiDocument.servers.unshift({
27
- description: "Counterfact",
28
- url,
29
- });
30
- // OpenApi 2 support:
31
- openApiDocument.host = url;
32
- // eslint-disable-next-line require-atomic-updates
33
- ctx.body = yaml.dump(openApiDocument);
34
- return;
35
- }
36
- // eslint-disable-next-line n/callback-return
37
- await next();
38
- };
39
- }
40
- function page(pathname, templateName, locals) {
41
- return async (ctx, next) => {
42
- const pathToHandlebarsTemplate = nodePath
43
- .join(__dirname, `../client/${templateName}.html.hbs`)
44
- .replaceAll("\\", "/");
45
- debug("pathToHandlebarsTemplate: %s", pathToHandlebarsTemplate);
46
- const render = Handlebars.compile(await readFile(pathToHandlebarsTemplate));
47
- if (ctx.URL.pathname === pathname) {
48
- ctx.body = render(locals);
49
- return;
50
- }
51
- // eslint-disable-next-line n/callback-return
52
- await next();
53
- };
54
- }
55
- export async function start(config) {
56
- const { basePath = process.cwd().replaceAll("\\", "/"), openApiPath = nodePath
57
- .join(basePath, "../openapi.yaml")
58
- .replaceAll("\\", "/"), port = DEFAULT_PORT, } = config;
59
- const app = new Koa();
60
- const { contextRegistry, koaMiddleware, registry } = await counterfact(basePath, openApiPath, config);
61
- app.use(openapi(openApiPath, `//localhost:${port}`));
62
- app.use(koaSwagger({
63
- routePrefix: "/counterfact/swagger",
64
- swaggerOptions: {
65
- url: "/counterfact/openapi",
66
- },
67
- }));
68
- debug("basePath: %s", basePath);
69
- debug("routes", registry.routes);
70
- app.use(page("/counterfact/", "index", {
71
- basePath,
72
- methods: ["get", "post", "put", "delete", "patch"],
73
- openApiHref: openApiPath.includes("://")
74
- ? openApiPath
75
- : pathToFileURL(openApiPath).href,
76
- openApiPath,
77
- routes: registry.routes,
78
- }));
79
- app.use(async (ctx, next) => {
80
- if (ctx.URL.pathname === "/counterfact") {
81
- ctx.redirect("/counterfact/");
82
- return;
83
- }
84
- if (ctx.URL.pathname === "/counterfact/stop") {
85
- debug("Stopping server...");
86
- await httpTerminator?.terminate();
87
- debug("Server stopped.");
88
- return;
89
- }
90
- // eslint-disable-next-line n/callback-return
91
- await next();
92
- });
93
- app.use(page("/counterfact/rapidoc", "rapi-doc", {
94
- basePath,
95
- routes: registry.routes,
96
- }));
97
- app.use(bodyParser());
98
- app.use(koaMiddleware);
99
- const server = app.listen({
100
- port,
101
- });
102
- httpTerminator = createHttpTerminator({
103
- server,
104
- });
105
- return { contextRegistry };
106
- }
@@ -1,133 +0,0 @@
1
- import type { OpenApiResponse } from "../src/server/response-builder.js";
2
-
3
- type OmitValueWhenNever<Base> = Pick<
4
- Base,
5
- {
6
- [Key in keyof Base]: [Base[Key]] extends [never] ? never : Key;
7
- }[keyof Base]
8
- >;
9
-
10
- type MediaType = `${string}/${string}`;
11
-
12
- interface OpenApiResponses {
13
- [key: string]: OpenApiResponse;
14
- }
15
-
16
- type IfHasKey<SomeObject, Key, Yes, No> = Key extends keyof SomeObject
17
- ? Yes
18
- : No;
19
-
20
- type MaybeShortcut<
21
- ContentType extends MediaType,
22
- Response extends OpenApiResponse,
23
- > = IfHasKey<
24
- Response["content"],
25
- ContentType,
26
- (body: Response["content"][ContentType]["schema"]) => ResponseBuilder<{
27
- content: Omit<Response["content"], ContentType>;
28
- headers: Response["headers"];
29
- }>,
30
- never
31
- >;
32
-
33
- type MatchFunction<Response extends OpenApiResponse> = <
34
- ContentType extends MediaType & keyof Response["content"],
35
- >(
36
- contentType: ContentType,
37
- body: Response["content"][ContentType]["schema"],
38
- ) => ResponseBuilder<{
39
- content: Omit<Response["content"], ContentType>;
40
- headers: Response["headers"];
41
- }>;
42
-
43
- type HeaderFunction<Response extends OpenApiResponse> = <
44
- Header extends string & keyof Response["headers"],
45
- >(
46
- header: Header,
47
- value: Response["headers"][Header]["schema"],
48
- ) => ResponseBuilder<{
49
- content: Response["content"];
50
- headers: Omit<Response["headers"], Header>;
51
- }>;
52
-
53
- export type ResponseBuilder<
54
- Response extends OpenApiResponse = OpenApiResponse,
55
- > = [keyof Response["content"]] extends [never]
56
- ? // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
57
- void
58
- : OmitValueWhenNever<{
59
- header: [keyof Response["headers"]] extends [never]
60
- ? never
61
- : HeaderFunction<Response>;
62
- html: MaybeShortcut<"text/html", Response>;
63
- json: MaybeShortcut<"application/json", Response>;
64
- match: [keyof Response["content"]] extends [never]
65
- ? never
66
- : MatchFunction<Response>;
67
- random: [keyof Response["content"]] extends [never] ? never : () => void;
68
- text: MaybeShortcut<"text/plain", Response>;
69
- }>;
70
-
71
- export type ResponseBuilderFactory<
72
- Responses extends OpenApiResponses = OpenApiResponses,
73
- > = {
74
- [StatusCode in keyof Responses]: ResponseBuilder<Responses[StatusCode]>;
75
- } & { [key: string]: ResponseBuilder<Responses["default"]> };
76
-
77
- export type HttpStatusCode =
78
- | 100
79
- | 101
80
- | 102
81
- | 200
82
- | 201
83
- | 202
84
- | 203
85
- | 204
86
- | 205
87
- | 206
88
- | 207
89
- | 226
90
- | 300
91
- | 301
92
- | 302
93
- | 303
94
- | 304
95
- | 305
96
- | 307
97
- | 308
98
- | 400
99
- | 401
100
- | 402
101
- | 403
102
- | 404
103
- | 405
104
- | 406
105
- | 407
106
- | 408
107
- | 409
108
- | 410
109
- | 411
110
- | 412
111
- | 413
112
- | 414
113
- | 415
114
- | 416
115
- | 417
116
- | 418
117
- | 422
118
- | 423
119
- | 424
120
- | 426
121
- | 428
122
- | 429
123
- | 431
124
- | 451
125
- | 500
126
- | 501
127
- | 502
128
- | 503
129
- | 504
130
- | 505
131
- | 506
132
- | 507
133
- | 511;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes