UNPKG

counterfact

Version:

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

133 lines (132 loc) 4.74 kB
import { RESERVED_WORDS } from "./reserved-words.js"; /** * Base class for all code-generation helpers in the TypeScript generator. * * A `Coder` wraps a single {@link Requirement} node from the OpenAPI spec and * knows how to emit TypeScript code for it. Subclasses override * {@link writeCode} to produce the actual source text. * * Coders are used by {@link Script} and {@link Repository} to lazily generate * exports and imports, resolving `$ref` references through the * {@link Specification} before writing. */ export class Coder { requirement; version; constructor(requirement, version = "") { this.requirement = requirement; this.version = version; } /** * A stable cache key for this coder, composed of the constructor name and * either the `$ref` value (for references) or the requirement URL. */ get id() { if (this.requirement.isReference) { return `${this.constructor.name}@${this.requirement.refUrl}`; } return `${this.constructor.name}@${this.requirement.url}`; } /** * Optional preamble emitted before the `export` keyword. * * Subclasses can return a string (e.g. a type alias) that must appear in the * output before this coder's export statement. * * @param _path - The path of the script being written (unused in base class). */ beforeExport(_path) { return ""; } /** * Returns a JSDoc comment block to be placed immediately before the export. * * Returns `""` by default; subclasses override this to surface OpenAPI * metadata (description, summary, examples, etc.). */ jsdoc() { return ""; } /** * Writes this coder's contribution to `script`. * * When the requirement is a `$ref`, delegates to {@link Script.import} so * the reference target is exported from its own module. Otherwise calls * {@link writeCode}. * * @param script - The script being assembled. * @returns The TypeScript source text for this coder's export value. */ write(script) { if (this.requirement.isReference) { return script.import(this); } return this.writeCode(script); } /** * Generates the TypeScript source text for this coder's value. * * This method is abstract — subclasses **must** override it. * * @param _script - The script being assembled. * @throws Always — callers should never reach the base implementation. */ writeCode(_script) { throw new Error("write() is abstract and should be overwritten by a subclass"); } /** * Resolves `$ref` references by returning the target coder. * * When this coder's requirement is not a reference, returns `this`. * Otherwise loads the referenced requirement and wraps it in an instance of * the same concrete coder class. */ async delegate() { if (!this.requirement.isReference) { return this; } const requirement = await this.requirement.reference(); return new this.constructor(requirement, this.version); } /** * Generator that yields candidate export names for this coder. * * The first name is derived from the last path segment of the requirement * URL, sanitised to be a valid TypeScript identifier. Subsequent names have * an incrementing numeric suffix to resolve collisions. * * @param rawName - Override the starting name (used by subclasses). */ *names(rawName = this.requirement.url.split("/").at(-1)) { const name = rawName .replace(/^\d/u, (digit) => `_${digit}`) .replaceAll(/[^\w$]/gu, "_"); const baseName = RESERVED_WORDS.has(name) ? `${name}_` : name; yield baseName; let index = 1; const MAX_NAMES_TO_GENERATE_BEFORE_GIVING_UP = 100; while (index < MAX_NAMES_TO_GENERATE_BEFORE_GIVING_UP) { index += 1; yield baseName + index; } } /** * Returns an optional TypeScript type annotation string to be placed between * the export name and its value (`export const name: <type> = value`). * * Returns `""` by default. */ typeDeclaration(_namespace, _script) { return ""; } /** * Returns the repository-relative path of the script where this coder's * export should live when imported by another script. * * Subclasses override this to place type exports in `types/paths/…` and * route exports in `routes/…`. */ modulePath() { return "did-not-override-coder-modulePath.ts"; } }