UNPKG

counterfact

Version:

a library for building a fake REST API for testing

138 lines (137 loc) 5.71 kB
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, }; }