UNPKG

counterfact

Version:

Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.

87 lines (86 loc) 3.2 kB
import { watch } from "chokidar"; import createDebug from "debug"; import { dereference } from "@apidevtools/json-schema-ref-parser"; import { applyOverlays } from "../util/apply-overlay.js"; import { waitForEvent } from "../util/wait-for-event.js"; import { sendTelemetry } from "../cli/telemetry.js"; import { CHOKIDAR_OPTIONS } from "./constants.js"; const debug = createDebug("counterfact:server:openapi-document"); /** * Represents a loaded OpenAPI document. Knows the location of its source * file, can read the file and initialize itself, can watch for file-system * changes, and dispatches a `"reload"` event (via `EventTarget`) whenever * the document is reloaded from disk. */ export class OpenApiDocument extends EventTarget { /** The path or URL of the OpenAPI source file. */ source; /** * Optional ordered list of overlay file paths/URLs to apply after each * load of the document. */ overlays; basePath; components; paths = {}; produces; watcher; constructor(source, overlays = []) { super(); this.source = source; this.overlays = overlays; } /** * Reads the source file and populates the document's properties. * Must be called at least once before the document data is accessible. */ async load() { try { const data = (await dereference(this.source)); if (this.overlays.length > 0) { await applyOverlays(data, this.overlays); } this.basePath = data.basePath; this.components = data.components; this.paths = data.paths; this.produces = data.produces; } catch (error) { debug("could not load OpenAPI document from %s: %o", this.source, error); const details = error instanceof Error ? error.message : String(error); throw new Error(`Could not load the OpenAPI spec from "${this.source}".\n${details}`, { cause: error }); } } /** * Starts watching the source file for changes. When a change is detected * the document reloads itself and dispatches a `"reload"` event. * * Has no effect when the source is `"_"` or a remote URL. */ async watch() { if (this.source === "_" || this.source.startsWith("http")) { return; } this.watcher = watch(this.source, CHOKIDAR_OPTIONS).on("change", () => { sendTelemetry("file_change_detected", { changeType: "change", fileType: "openapi", }); void (async () => { try { await this.load(); debug("reloaded OpenAPI document from %s", this.source); this.dispatchEvent(new Event("reload")); } catch (error) { debug("failed to reload OpenAPI document from %s: %o", this.source, error); } })(); }); await waitForEvent(this.watcher, "ready"); } /** Stops watching the source file. */ async stopWatching() { await this.watcher?.close(); } }