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.
- package/bin/counterfact.js +17 -22
- package/dist/server/app.js +76 -0
- package/dist/server/code-generator.js +23 -0
- package/dist/server/create-koa-app.js +49 -0
- package/dist/{src/server → server}/dispatcher.js +1 -2
- package/dist/{src/server → server}/koa-middleware.js +10 -2
- package/dist/server/openapi-middleware.js +22 -0
- package/dist/server/page-middleware.js +24 -0
- package/dist/{src/server → server}/repl.js +1 -0
- package/dist/{src/server → server}/response-builder.js +2 -1
- package/dist/{src/server → server}/transpiler.js +0 -1
- package/dist/{src/typescript-generator → typescript-generator}/generate.js +11 -2
- package/dist/{src/typescript-generator → typescript-generator}/repository.js +3 -8
- package/dist/{src/typescript-generator → typescript-generator}/response-type-coder.js +5 -9
- package/package.json +17 -17
- package/dist/src/server/counterfact.js +0 -47
- package/dist/src/server/start.js +0 -106
- package/dist/templates/response-builder-factory.ts +0 -133
- /package/dist/{src/client → client}/index.html.hbs +0 -0
- /package/dist/{src/client → client}/rapi-doc.html.hbs +0 -0
- /package/dist/{src/migrations → migrations}/0.27.js +0 -0
- /package/dist/{templates/response-builder-factory.js → server/config.js} +0 -0
- /package/dist/{src/server → server}/constants.js +0 -0
- /package/dist/{src/server → server}/context-registry.js +0 -0
- /package/dist/{src/server → server}/module-loader.js +0 -0
- /package/dist/{src/server → server}/registry.js +0 -0
- /package/dist/{src/server → server}/tools.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/coder.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/context-coder.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/context-type-coder.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/operation-coder.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/operation-type-coder.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/parameters-type-coder.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/printers.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/requirement.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/schema-coder.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/schema-type-coder.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/script.js +0 -0
- /package/dist/{src/typescript-generator → typescript-generator}/specification.js +0 -0
- /package/dist/{src/util → util}/ensure-directory-exists.js +0 -0
- /package/dist/{src/util → util}/read-file.js +0 -0
package/bin/counterfact.js
CHANGED
|
@@ -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/
|
|
10
|
-
import {
|
|
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("
|
|
54
|
+
debug("loading counterfact (%o)", config);
|
|
62
55
|
|
|
63
|
-
const {
|
|
56
|
+
const { start } = await counterfact(config);
|
|
64
57
|
|
|
65
|
-
debug("
|
|
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
|
-
|
|
86
|
+
debug("starting server");
|
|
94
87
|
|
|
95
|
-
|
|
88
|
+
await start();
|
|
96
89
|
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
:
|
|
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) => ({
|
|
@@ -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("
|
|
35
|
-
const paths = await specification
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
79
|
+
return text;
|
|
84
80
|
}
|
|
85
81
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "counterfact",
|
|
3
|
-
"version": "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
|
|
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.
|
|
48
|
-
"@stryker-mutator/jest-runner": "7.
|
|
49
|
-
"@stryker-mutator/typescript-checker": "7.
|
|
50
|
-
"@swc/core": "1.3.
|
|
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.
|
|
54
|
-
"@types/js-yaml": "4.0.
|
|
55
|
-
"@types/koa": "2.13.
|
|
56
|
-
"@types/koa-bodyparser": "4.3.
|
|
57
|
-
"@types/koa-proxy": "1.0.
|
|
58
|
-
"@types/koa-static": "4.0.
|
|
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.
|
|
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.
|
|
67
|
-
"eslint-plugin-jest": "27.
|
|
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": "
|
|
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.
|
|
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
|
-
}
|
package/dist/src/server/start.js
DELETED
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|