counterfact
Version:
a library for building a fake REST API for testing
138 lines (137 loc) • 5.71 kB
JavaScript
import fs, { rm } from "node:fs/promises";
import nodePath from "node:path";
import { dereference } from "@apidevtools/json-schema-ref-parser";
import { createHttpTerminator } from "http-terminator";
import yaml from "js-yaml";
import { startRepl } from "./repl/repl.js";
import { ContextRegistry } from "./server/context-registry.js";
import { createKoaApp } from "./server/create-koa-app.js";
import { Dispatcher, } from "./server/dispatcher.js";
import { koaMiddleware } from "./server/koa-middleware.js";
import { ModuleLoader } from "./server/module-loader.js";
import { Registry } from "./server/registry.js";
import { Transpiler } from "./server/transpiler.js";
import { CodeGenerator } from "./typescript-generator/code-generator.js";
import { readFile } from "./util/read-file.js";
const allowedMethods = [
"all",
"head",
"get",
"post",
"put",
"delete",
"patch",
"options",
];
export async function loadOpenApiDocument(source) {
try {
const text = await readFile(source);
const openApiDocument = await yaml.load(text);
return (await dereference(openApiDocument));
}
catch {
return undefined;
}
}
const mswHandlers = {};
export async function handleMswRequest(request) {
const { method, rawPath } = request;
const handler = mswHandlers[`${method}:${rawPath}`];
if (handler) {
return handler(request);
}
console.warn(`No handler found for ${method} ${rawPath}`);
return { error: `No handler found for ${method} ${rawPath}`, status: 404 };
}
export async function createMswHandlers(config, ModuleLoaderClass = ModuleLoader) {
// TODO: For some reason the Vitest Custom Commands needed by Vitest Browser mode fail on fs.readFile when they are called from the nested loadOpenApiDocument function.
// If we "pre-read" the file here it works. This is a workaround to avoid the issue.
const _ = await fs.readFile(config.openApiPath);
const openApiDocument = await loadOpenApiDocument(config.openApiPath);
if (openApiDocument === undefined) {
throw new Error(`Could not load OpenAPI document from ${config.openApiPath}`);
}
const modulesPath = config.basePath;
const compiledPathsDirectory = nodePath
.join(modulesPath, ".cache")
.replaceAll("\\", "/");
const registry = new Registry();
const contextRegistry = new ContextRegistry();
const dispatcher = new Dispatcher(registry, contextRegistry, openApiDocument, config);
const moduleLoader = new ModuleLoaderClass(compiledPathsDirectory, registry, contextRegistry);
await moduleLoader.load();
const routes = registry.routes;
const handlers = routes.flatMap((route) => {
const { methods, path } = route;
return Object.keys(methods)
.filter((method) => allowedMethods.includes(method.toLowerCase()))
.map((method) => {
const lowerMethod = method.toLowerCase();
const apiPath = `${openApiDocument.basePath ?? ""}${path.replaceAll("{", ":").replaceAll("}", "")}`;
const handler = async (request) => {
return await dispatcher.request(request);
};
mswHandlers[`${method}:${apiPath}`] = handler;
return { method: lowerMethod, path: apiPath };
});
});
return handlers;
}
export async function counterfact(config) {
const modulesPath = config.basePath;
const compiledPathsDirectory = nodePath
.join(modulesPath, ".cache")
.replaceAll("\\", "/");
await rm(compiledPathsDirectory, { force: true, recursive: true });
const registry = new Registry();
const contextRegistry = new ContextRegistry();
const codeGenerator = new CodeGenerator(config.openApiPath, config.basePath, config.generate);
const dispatcher = new Dispatcher(registry, contextRegistry, await loadOpenApiDocument(config.openApiPath), config);
const transpiler = new Transpiler(nodePath.join(modulesPath, "routes").replaceAll("\\", "/"), compiledPathsDirectory, "commonjs");
const moduleLoader = new ModuleLoader(compiledPathsDirectory, registry, contextRegistry);
const middleware = koaMiddleware(dispatcher, config);
const koaApp = createKoaApp(registry, middleware, config);
async function start(options) {
const { generate, startRepl: shouldStartRepl, startServer, watch, buildCache, } = options;
if (generate.routes || generate.types) {
await codeGenerator.generate();
}
if (watch.routes || watch.types) {
await codeGenerator.watch();
}
let httpTerminator;
if (startServer) {
await transpiler.watch();
await moduleLoader.load();
await moduleLoader.watch();
const server = koaApp.listen({
port: config.port,
});
httpTerminator = createHttpTerminator({
server,
});
}
else if (buildCache) {
// If we are not starting the server, we still want to transpile and load modules
await transpiler.watch();
await transpiler.stopWatching();
}
const replServer = shouldStartRepl && startRepl(contextRegistry, config);
return {
replServer,
async stop() {
await codeGenerator.stopWatching();
await transpiler.stopWatching();
await moduleLoader.stopWatching();
await httpTerminator?.terminate();
},
};
}
return {
contextRegistry,
koaApp,
koaMiddleware: middleware,
registry,
start,
};
}