import type { SourceFormat } from '@amritk/adapters/source-format'; import type { UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy'; /** * Configuration for the CLI tool. * These properties map 1:1 with CLI flags and config file keys. */ export type CliConfig = { /** Path to the schema to process — a JSON Schema file, or a JS/TS module when `input` is set. */ readonly schema?: string; /** * Path to a directory of JSON Schema files. When set, the CLI walks the directory * recursively, generates parsers for every `*.json` schema it finds, and mirrors the * directory layout under `outDir`. A single shared `_helpers/` directory is emitted at * the root of `outDir` (in embedded mode) and every nested parser imports from it. * Mutually exclusive with `schema`; when both are present `schemaDir` wins. */ readonly schemaDir?: string; /** * The source format of the schema. Defaults to `'json'` (a plain JSON Schema file). * * `'avro'` reads `schema` as a `.avsc` JSON document and converts it — no module * is loaded, so `export` does not apply. Every other format loads `schema` as a * module and converts the exported value via the matching adapter. */ readonly input?: SourceFormat; /** * Which export of the schema module to use when `input` names a module format * (anything but `'json'` and `'avro'`, which are parsed as data). * Defaults to the default export, or the sole named export when there is exactly one. */ readonly export?: string; /** Output directory for generated TypeScript files. Mutually exclusive with `outFile`. */ readonly outDir?: string; /** * Output everything to a single TypeScript file instead of a directory of files. * Mutually exclusive with `outDir`. Currently supported only with `typesOnly`. */ readonly outFile?: string; /** * When true, only generate TypeScript type definitions without parser functions. * Useful when you only need the type shapes and do not need runtime validation. */ readonly typesOnly?: boolean; /** * When true, emit the validator half and no parser: the type, `isX`, * `validateX`, and whichever of `checkX`/`coerceX`/`repairX` were asked for. * * Implies `validators`, and is the mirror of `typesOnly` at the other end of * the ladder — one run with nothing that rewrites a document, the other with * nothing that executes at all. Without it every run carries a parser, so the * library's own default (a type and its validators, nothing that builds a * value) had no spelling here. */ readonly validatorsOnly?: boolean; /** * When true, also emit validation functions alongside the parsers. For every * generated type `X` the CLI writes a `validateX` (returning a rich * `ValidationResult` with JSON-Pointer error paths) and an `isX` boolean type * guard. * * They share one directory and one declaration of `X` with the parser: the * type and the validators land in `x.ts`, and the parser moves to `x.parse.ts` * beside them, importing the type from there. Both halves are then talking * about the same type, which two side-by-side trees could only promise. * * Works with both `schema` and `schemaDir`. Incompatible with `typesOnly` and * `outFile`, which produce no runtime code. */ readonly validators?: boolean; /** * When true, generated validators also get a `checkX`: the same * `ValidationResult` `validateX` returns, carrying the one error that stopped * it. It gives up at the first violation instead of walking on to collect the * rest, so it costs a single error object where `validateX` costs however many * the document earns. * * Reach for it when a failure has to be reported but only the first thing * wrong matters — a service refusing to boot on a bad config does not need the * other nine. When nothing has to be reported at all, `isX` is cheaper still: * it builds no error object. * * Requires `validators`. `validateX` and `isX` are unchanged either way. */ readonly check?: boolean; /** * When true, generated validators also get a `coerceX`: it moves scalars * toward what the schema asks for and then runs the very same `validateX` over * the result, so a config value written the YAML way reads the same however it * arrived. * * The input is never modified, and nothing is ever substituted: where a value * cannot be coerced into a valid one it is handed to the validator untouched, * so the error names what the caller actually wrote, with the keyword and * params that rejected it. That is the difference from a coercing *parser*, * which repairs toward a default and leaves nothing to report. * * The table is Ajv's `coerceTypes` minus the cells where Ajv guesses — no * whitespace-to-zero, no `0x`/`Infinity` strings, and nothing coerced to or * from `null`. `null` is left alone deliberately: it usually means "not set", * and reading it as `""`, `0` or `false` erases that. Every scalar this * coerces, Ajv coerces to the same value; the cells Ajv guesses at become * errors instead of silent repairs. * * Coercion reaches through `$ref`, `allOf`, `if`/`then`/`else` and unions. At * a union (`anyOf`, `oneOf`, a `type` array) a value that already matches a * branch as written is left alone. Otherwise each branch coerces it its own * way, and the result is taken only when every branch that then accepts it * agrees on it; two different readings leave the value as written for the * validator to report. So the answer never depends on the order the union was * written in, which under Ajv it does — Ajv coerces into the first branch that * will take the value, and turns `false` into `"false"` through a string * branch listed before a `const: false` one. * * A document that is already valid is answered by `isX` alone where that is a * standalone guard, and handed back as the very same object. Where the caller * cannot let Ajv mutate its input, so Ajv has to clone it first, `coerceX` is * faster, by the widest margin on valid input (`bench:validators:coerce`). * Where the caller owns the document and lets Ajv * coerce it in place, there is no clone to save, and on a large document Ajv * can come out modestly ahead. * * Requires `validators`. `validateX` and `isX` are unchanged either way. */ readonly coerce?: boolean; /** * When true, generated validators also get a `repairX`: it coerces, validates, * and then repairs each position the validator rejected to a value the schema * itself supplies — a `default`, a `const`, the first `enum` member, or a * fallback built to satisfy the position's own bounds. The result carries the * repaired value together with `repairs`, which *are* the errors `validateX` * produced for the positions that were repaired, so a caller logs the same * path, keyword and params it would have been rejected with. * * A document needing no repair comes back valid with an empty `repairs`; one * fully repaired comes back valid with a non-empty one, leaving the caller to * decide whether that is acceptable; one that could not be fully repaired * comes back invalid carrying both the repairs applied and the errors still * outstanding. * * Repair is an autofix tool, not a stricter validator: it accepts documents * `validateX` rejects, by repairing them. Accepting only `valid` with an empty * `repairs` gives exactly the verdicts `coerceX` gives, so a config loader that * must reject a typo wants `coerceX`, and `repairX` is for producing a fixed * document. * * Implies `coerce` — repairing runs after coercion, so a value merely written * in the wrong type is right before the validator sees it and never counts as * a repair. Requires `validators`. */ readonly repair?: boolean; /** * When true, a failing `anyOf` / `oneOf` in a generated validator also reports * the errors of the branch it meant, instead of the bare "must match a schema * in anyOf" that names no field and no reason. * * Off by default, and off costs nothing — the generated code is exactly what * it would be without the option. On, a combinator that passes costs what it * costs with the option off: its branches are tested by the same functions. * Only an `anyOf` / `oneOf` that has already failed asks its branches again, * this time for what each one complained about, so the price is paid on * invalid input alone. * * Requires `validators`. */ readonly branchErrors?: boolean; /** * When true, also emit test-data files for every schema: a `fast-check` * arbitrary (`FooArbitrary`) that produces schema-valid values and a concrete * `fooExample` value. The files are written into an `examples/` subdirectory of * the output destination (mirroring the schema layout under `--schema-dir`) so * they never collide with the parser output. The generated arbitraries import * `fast-check`, which consumers must install as a (dev) dependency. */ readonly examples?: boolean; /** * When true, also emit a `@amritk/api` channel contract per AsyncAPI channel * into a `contracts/` subdirectory: one `defineMessages({ ... })` module * naming every message that channel carries, keyed by its wire discriminator * value, plus a barrel. Requires `input: 'asyncapi'` — the contracts are * projected from the document's channels and operations, which no other input * format has. Incompatible with `typesOnly`: a contract is a runtime value. * * The generated modules import `@amritk/api`, which the *consuming* project * must have installed; it is not a dependency of the generated parsers. */ readonly messageContracts?: boolean; /** * Property name that says which message a frame is, for the contracts * `messageContracts` emits. Defaults to `'type'`, matching `@amritk/api`. * A channel that declares its own via `x-mjst: { discriminator }` keeps it — * one flag covers a whole run, and a run may span channels that disagree. */ readonly discriminator?: string; /** * When true, compile the generated TypeScript files to .js and .d.ts output. * The .ts source files are removed after compilation. */ readonly build?: boolean; /** * @deprecated No longer has any effect, and is accepted only so existing * scripts and config files keep running. Generated files always replace * whatever is at their path; version control is where an unwanted replacement * shows up and gets reverted. `build` still only ever deletes the intermediate * sources the run itself committed. */ readonly force?: boolean; /** * When true, the generated parsers emit a console.warn for every input key * that is not declared in the schema's properties. */ readonly logWarnings?: boolean; /** * When true, the generated parsers throw on type/shape mismatches * (wrong type, missing required property, enum/pattern/min/max violations) * instead of coercing invalid input to default values. */ readonly strict?: boolean; /** * When true, generated parsers build their result from declared properties * only, silently dropping any undeclared input key at every nesting level * (zod's `.strip()`). Extras are never a validation error, so this composes * with `strict` (which still throws on wrong types and missing required * properties) and yields to `additionalProperties: false`, which rejects * rather than strips in strict mode. */ readonly stripUnknown?: boolean; /** * When true, the generated coercing parsers normalize a mis-cased string to * the exact casing of a declared `enum`/`const` member it matches * case-insensitively (e.g. `hElLo` → `hello`) instead of coercing to the * default. Coerce mode only — `strict` parsers still reject a casing * mismatch. Correctly-cased input keeps the exact-match fast path, so the hot * path is unaffected. */ readonly caseInsensitive?: boolean; /** * How generated fast paths prove a closed object (`additionalProperties: * false`) carries no undeclared key. `'count-keys'` (the default) compares * `Object.keys(obj).length` and is the faster form on JavaScriptCore (Bun); * `'count-enumerable'` counts with `for…in` and is the faster form on V8 * (Node). Applies to parsers and validators alike. */ readonly unknownKeys?: UnknownKeysStrategy; /** * String `format`s the generated validators enforce — `'all'`, or the names to * check. Left unset, `format` stays an annotation, which is 2020-12's own * reading and what `@amritk/runtime-validators` does when given no formats. * * Set it to whatever the code validating the same schemas at runtime uses, so * the build-time and runtime answers agree. */ readonly formats?: 'all' | readonly string[]; /** * Controls how generated parsers reference their runtime helpers. * - `'package'`: emit `import ... from '@amritk/helpers/...'`. * - `'embedded'`: ship the helper source under `outDir/_helpers/` and emit * relative imports so the output directory is self-contained. * * When omitted, the CLI auto-detects: it picks `'package'` if `@amritk/helpers` * resolves from outDir, otherwise falls back to `'embedded'`. */ readonly helpers?: 'package' | 'embedded'; /** * When true, every property, array, and record in the generated type * definitions is emitted as `readonly`, producing deeply immutable types. */ readonly readonly?: boolean; /** * Suffix appended to every generated type name derived from a `$ref` * (e.g. `'Object'` turns `Contact` into `ContactObject`). Defaults to `''` * (no suffix). The root type name is used verbatim and is unaffected. */ readonly typeSuffix?: string; /** * Prepends a comment header to every generated file (excluding `_helpers/`). * - `true` — use the default message: "This file was auto-generated by @amritk/mjst…" * - A string — use that text as the message body (wrapped in a JSDoc block). * - `false` / omitted — no header. */ readonly banner?: boolean | string; /** * Extension emitted on every relative import specifier in the generated * output (cross-file `$ref` imports, the index barrel, and embedded-helper * imports). * - `'ts'` (default): the literal on-disk paths, so the generated `.ts` * sources load under Bun, Node's type stripping (Node 22.6+ with * `--experimental-strip-types`, unflagged from 22.18/23), and tsc with * `allowImportingTsExtensions`. Incompatible with `build` — tsc refuses * to emit from `.ts` specifiers. * - `'js'`: the standard TS NodeNext form (`./x.js` resolving to a sibling * `x.ts`), for output that will be compiled. Used automatically when * `build` is set and no extension was chosen explicitly. */ readonly importExt?: 'js' | 'ts'; /** * Name for the root type of a single-`schema` run (e.g. `'Program'` yields * `parseProgram` / `validateProgram`). When omitted, the name is * derived from the schema's `title`, falling back to the schema filename in * PascalCase (`spec-plan.json` → `SpecPlan`) and then to `'Document'`. * Not supported with `schemaDir`, where each schema names its own root. */ readonly rootType?: string; /** * Allow fetching `http(s)` `$ref`s while dereferencing a schema that contains * cross-file or remote references. Defaults to `false`: generation stays * offline unless asked, so a schema with a remote `$ref` fails rather than * making a network call. A non-empty `allowedHosts` implies this. */ readonly resolveRemote?: boolean; /** * If set, only these hosts (e.g. `api.example.com`) may be fetched for remote * `$ref`s. Restricting to an allow-list is itself an opt-in to remote fetching * (it implies `resolveRemote`), and each listed host bypasses the private-host * guard. */ readonly allowedHosts?: readonly string[]; /** * Permit remote `$ref`s to private, loopback, and link-local hosts. Defaults * to `false`: such targets are refused as a best-effort SSRF guard (notably the * `169.254.169.254` cloud-metadata endpoint). An explicit `allowedHosts` entry * always bypasses this guard. */ readonly allowPrivateHosts?: boolean; /** * Extra directories a local (cross-file) `$ref` may resolve into. A local * `$ref` is confined to the schema's own directory by default, so a * `{"$ref": "../../../secrets.env"}` reads nothing; this is the escape hatch for * the ordinary split-spec layout where `v1/api.json` refers to a shared * `common/user.json` one level up. * * The schema's own directory is always allowed, so the listed roots widen the * default rather than replacing it. Entries are resolved against the process * working directory — including entries that came from a config file, which * matches how `schema` and `outDir` are treated. */ readonly allowedRoots?: readonly string[]; };