@amritk/mjst 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AI.md CHANGED
@@ -8,7 +8,7 @@ reference is [README.md](./README.md); config options are in
8
8
 
9
9
  > Pre-alpha: generated output and flags change without notice pre-1.0. This
10
10
  > package ships **only a `mjst` binary** — there are no JS exports. For the
11
- > programmatic API import `@amritk/generate-parsers` etc. instead.
11
+ > programmatic API import `@amritk/validation` etc. instead.
12
12
 
13
13
  ## The four commands
14
14
 
@@ -57,11 +57,19 @@ mjst markdown ./config.schema.json --table --readme ./README.md
57
57
  `--discriminator <prop>` when the document tags frames with something other
58
58
  than `type`; a channel carrying `x-mjst: { discriminator }` keeps its own.
59
59
 
60
+ ## Picking what gets emitted
61
+
62
+ Every run emits the type. `--types-only` stops there. A bare run adds a parser;
63
+ `--validators` adds `isX`/`validateX` beside it; `--validators-only` emits those
64
+ *without* a parser, which is the shape to reach for when the generated code only
65
+ ever judges input it did not produce. `--check`, `--coerce` and `--repair` each
66
+ add one more entry point to the validator half and work under either flag.
67
+
60
68
  ## Common flags
61
69
 
62
70
  `--schema` / `--schema-dir`, `--out-dir` / `--out-file`, `--input`,
63
- `--validators`, `--examples`, `--message-contracts`, `--discriminator`,
64
- `--types-only`, `--build`, `--strict`, `--strip-unknown`,
71
+ `--validators` / `--validators-only`, `--examples`, `--message-contracts`,
72
+ `--discriminator`, `--types-only`, `--build`, `--strict`, `--strip-unknown`,
65
73
  `--unknown-keys count-keys|count-enumerable`, `--readonly`,
66
74
  `--import-ext ts|js`, `--config <path>`, `--resolve-remote` /
67
75
  `--allowed-hosts` (SSRF-guarded remote `$ref`s).
package/README.md CHANGED
@@ -95,19 +95,35 @@ JSON-Pointer error paths — and an `isX(input)` boolean type guard:
95
95
  npx mjst --schema ./schema.json --out-dir ./generated --validators
96
96
  ```
97
97
 
98
- The validators carry the same schema-derived filenames as the parsers, so they land in a
99
- `validators/` subdirectory to avoid colliding:
98
+ Both halves land in one directory, over a single declaration of each type. The type and the
99
+ validators share the file named after the schema; the parser moves next to them under a
100
+ `.parse` suffix and imports the type from there:
100
101
 
101
102
  ```
102
103
  generated/
103
- schema.ts, index.ts ← parsers + types
104
- validators/
105
- schema.ts, index.ts ← validateX / isX
106
- validation-result.ts ← ValidationResult / ValidationError contract
104
+ schema.ts ← export type X, plus isX / validateX
105
+ schema.parse.ts ← parseX, importing the type from ./schema
106
+ validation-result.ts ← ValidationResult / ValidationError contract
107
+ index.ts ← a barrel over all of it
107
108
  ```
108
109
 
109
- This works with `--schema-dir` too (the `validators/` tree mirrors the parser layout) and with
110
- `--build`. It cannot be combined with `--types-only` or `--out-file`, which emit no runtime code.
110
+ So there is exactly one `export type X` in the output, and the guard, the validator and the
111
+ parser are all talking about it.
112
+
113
+ This works with `--schema-dir` and `--input asyncapi` too — each schema's own subdirectory
114
+ carries both halves — and with `--build`. It cannot be combined with `--types-only` or
115
+ `--out-file`, which emit no runtime code.
116
+
117
+ Ask for no validators and there is nothing to collide with, so the parser keeps `schema.ts` and
118
+ its own type, and no `validation-result.ts` is emitted at all.
119
+
120
+ Three flags shape the validators, and each needs `--validators` alongside it:
121
+
122
+ - `--check` adds `checkX`, the same `ValidationResult` as `validateX` but stopped at the first
123
+ violation, so it costs one error object rather than however many the document earns.
124
+ - `--coerce` adds `coerceX`, which moves scalars toward the declared type and then validates.
125
+ - `--repair` adds `repairX`, which coerces, validates, and then repairs each rejected position
126
+ to a value the schema supplies.
111
127
 
112
128
  ### Running the generated output
113
129
 
@@ -305,6 +321,10 @@ npx mjst markdown ./config.schema.json --table --readme ./README.md
305
321
  | `--layout` | Default layout for nested properties: `headings` (default), `table`, or `none`. |
306
322
  | `--sort` | Property order: `schema` (default) or `alphabetical`. |
307
323
  | `--heading-level` | Heading level of the page title, 1-6 (default: `1`). |
324
+ | `--type-column` | When a property table renders its **Type** column: `auto` (default — only when a row fills it with something a reader could act on), `always`, or `never`. |
325
+ | `--default-column` | When a property table renders its **Default** column: `auto` (default), `always`, or `never`. |
326
+ | `--required-style` | How a property table says which properties are required: `marker` (default — `_required_` beside the name) or `column` (a **Required** column). |
327
+ | `--required-first` | List the required properties at the top of every table, the rest under them. |
308
328
  | `--table` | Render the HTML config table instead of the prose pages. |
309
329
  | `--readme` | Markdown file the table is spliced into (default: `README.md`). Requires `--table`. |
310
330
 
@@ -389,7 +409,52 @@ Under `--table` only the content between `<!-- config-table-start -->
389
409
  <td align="center"><code>false</code></td>
390
410
  </tr>
391
411
  <tr>
392
- <td colspan="4">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. The files land in a validators/ subdirectory of the output so they never collide with the parser files, which share the same schema-derived names. Works with both schema and schemaDir. Incompatible with typesOnly and outFile, which produce no runtime code.</td>
412
+ <td colspan="4">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. Works with both schema and schemaDir. Incompatible with typesOnly and outFile, which produce no runtime code.</td>
413
+ </tr>
414
+ <tr>
415
+ <td>🛡️ <code>validatorsOnly</code></td>
416
+ <td><code>--validators-only</code></td>
417
+ <td><code>boolean</code></td>
418
+ <td align="center"><code>false</code></td>
419
+ </tr>
420
+ <tr>
421
+ <td colspan="4">Emit the validator half and no parser: the type, isX, validateX, and whichever of checkX, coerceX and repairX were asked for. Implies validators, so it does not need it as well. This 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 — and it is what to reach for when the generated code only ever judges input it did not produce. Without it every run carries a parser. Incompatible with typesOnly and outFile.</td>
422
+ </tr>
423
+ <tr>
424
+ <td>🔎 <code>check</code></td>
425
+ <td><code>--check</code></td>
426
+ <td><code>boolean</code></td>
427
+ <td align="center"><code>false</code></td>
428
+ </tr>
429
+ <tr>
430
+ <td colspan="4">Also emit a checkX alongside each validateX. It is the same ValidationResult, carrying the one error that stopped it: where validateX walks the whole document to collect every violation, checkX gives up at the first and costs a single error object. 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, since it builds no error object. validateX and isX are unchanged either way. Requires validators.</td>
431
+ </tr>
432
+ <tr>
433
+ <td>🧪 <code>coerce</code></td>
434
+ <td><code>--coerce</code></td>
435
+ <td><code>boolean</code></td>
436
+ <td align="center"><code>false</code></td>
437
+ </tr>
438
+ <tr>
439
+ <td colspan="4">Also emit a coerceX alongside each validateX. It moves scalars toward the type the schema declares and then runs the very same validateX over the result, so a config value written the YAML way reads the same whether it came from a config file or an extension. The input is never modified and nothing is substituted: a value that cannot be coerced into a valid one reaches the validator untouched, so the error names what the caller actually wrote, with the keyword and params that rejected it. The table is Ajv's coerceTypes minus the cells where Ajv guesses — no whitespace-to-zero, no hex or Infinity strings, and nothing coerced to or from null — so every value this coerces, Ajv coerces to the same value, and the rest become errors rather than silent repairs. At a union it coerces only when exactly one of the offered types can take the value, and leaves a value that is already one of them alone, so the answer does not depend on the order the union was written in. Requires validators.</td>
440
+ </tr>
441
+ <tr>
442
+ <td>🔧 <code>repair</code></td>
443
+ <td><code>--repair</code></td>
444
+ <td><code>boolean</code></td>
445
+ <td align="center"><code>false</code></td>
446
+ </tr>
447
+ <tr>
448
+ <td colspan="4">Also emit a repairX alongside each validateX. It coerces, runs the very same validateX, 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 that position's own bounds — re-validating until the document is accepted or nothing further can be repaired. The repairs it reports are the validator's own errors, the ones a repair was found for, so a caller logs the same path, keyword and params it would have been rejected with and the two can never drift apart. A document needing nothing comes back valid with an empty repairs list; 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. The input is never modified and everything the repair did not touch is shared. Implies coerce, so a value merely written in the wrong type is right before the validator sees it and never counts as a repair. Requires validators.</td>
449
+ </tr>
450
+ <tr>
451
+ <td>🌿 <code>branchErrors</code></td>
452
+ <td><code>--branch-errors</code></td>
453
+ <td><code>boolean</code></td>
454
+ <td align="center"><code>false</code></td>
455
+ </tr>
456
+ <tr>
457
+ <td colspan="4">Explain a failing anyOf/oneOf with the errors of the branch it meant, instead of the bare "must match a schema in anyOf" that names no field and no reason. The branch that rejected the value's kind is dropped first, so a string-or-object union is left with the one branch that was talking about this value; where several remain, a discriminator breaks the tie; where none stands out, nothing extra is reported. Off by default and off costs nothing — the generated code is exactly what it would be without the option. On, each branch keeps what it complained about: a push per failing branch and one lazily-created array per combinator that had one. Requires validators.</td>
393
458
  </tr>
394
459
  <tr>
395
460
  <td>🎲 <code>examples</code></td>
@@ -686,7 +751,7 @@ usage error. Run `mjst markdown --help` for details.
686
751
  <td align="center"><code>false</code></td>
687
752
  </tr>
688
753
  <tr>
689
- <td colspan="4">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. The files land in a validators/ subdirectory of the output so they never collide with the parser files, which share the same schema-derived names. Works with both schema and schemaDir. Incompatible with typesOnly and outFile, which produce no runtime code.</td>
754
+ <td colspan="4">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. Works with both schema and schemaDir. Incompatible with typesOnly and outFile, which produce no runtime code.</td>
690
755
  </tr>
691
756
  <tr>
692
757
  <td>🎲 <code>examples</code></td>
@@ -41,16 +41,106 @@ export type CliConfig = {
41
41
  * Useful when you only need the type shapes and do not need runtime validation.
42
42
  */
43
43
  readonly typesOnly?: boolean;
44
+ /**
45
+ * When true, emit the validator half and no parser: the type, `isX`,
46
+ * `validateX`, and whichever of `checkX`/`coerceX`/`repairX` were asked for.
47
+ *
48
+ * Implies `validators`, and is the mirror of `typesOnly` at the other end of
49
+ * the ladder — one run with nothing that rewrites a document, the other with
50
+ * nothing that executes at all. Without it every run carries a parser, so the
51
+ * library's own default (a type and its validators, nothing that builds a
52
+ * value) had no spelling here.
53
+ */
54
+ readonly validatorsOnly?: boolean;
44
55
  /**
45
56
  * When true, also emit validation functions alongside the parsers. For every
46
57
  * generated type `X` the CLI writes a `validateX` (returning a rich
47
58
  * `ValidationResult` with JSON-Pointer error paths) and an `isX` boolean type
48
- * guard. The files land in a `validators/` subdirectory of the output so they
49
- * never collide with the parser files, which share the same schema-derived
50
- * names. Works with both `schema` and `schemaDir`. Incompatible with
51
- * `typesOnly` and `outFile`, which produce no runtime code.
59
+ * guard.
60
+ *
61
+ * They share one directory and one declaration of `X` with the parser: the
62
+ * type and the validators land in `x.ts`, and the parser moves to `x.parse.ts`
63
+ * beside them, importing the type from there. Both halves are then talking
64
+ * about the same type, which two side-by-side trees could only promise.
65
+ *
66
+ * Works with both `schema` and `schemaDir`. Incompatible with `typesOnly` and
67
+ * `outFile`, which produce no runtime code.
52
68
  */
53
69
  readonly validators?: boolean;
70
+ /**
71
+ * When true, generated validators also get a `checkX`: the same
72
+ * `ValidationResult` `validateX` returns, carrying the one error that stopped
73
+ * it. It gives up at the first violation instead of walking on to collect the
74
+ * rest, so it costs a single error object where `validateX` costs however many
75
+ * the document earns.
76
+ *
77
+ * Reach for it when a failure has to be reported but only the first thing
78
+ * wrong matters — a service refusing to boot on a bad config does not need the
79
+ * other nine. When nothing has to be reported at all, `isX` is cheaper still:
80
+ * it builds no error object.
81
+ *
82
+ * Requires `validators`. `validateX` and `isX` are unchanged either way.
83
+ */
84
+ readonly check?: boolean;
85
+ /**
86
+ * When true, generated validators also get a `coerceX`: it moves scalars
87
+ * toward what the schema asks for and then runs the very same `validateX` over
88
+ * the result, so a config value written the YAML way reads the same however it
89
+ * arrived.
90
+ *
91
+ * The input is never modified, and nothing is ever substituted: where a value
92
+ * cannot be coerced into a valid one it is handed to the validator untouched,
93
+ * so the error names what the caller actually wrote, with the keyword and
94
+ * params that rejected it. That is the difference from a coercing *parser*,
95
+ * which repairs toward a default and leaves nothing to report.
96
+ *
97
+ * The table is Ajv's `coerceTypes` minus the cells where Ajv guesses — no
98
+ * whitespace-to-zero, no `0x`/`Infinity` strings, and nothing coerced to or
99
+ * from `null`. Every value this coerces, Ajv coerces to the same value, so
100
+ * moving off Ajv never changes a value; it turns some of Ajv's silent repairs
101
+ * into errors instead. At a union it coerces only when exactly one of the
102
+ * offered types can take the value, and leaves a value that is already one of
103
+ * them alone — so the answer does not depend on the order the union is
104
+ * written in, which under Ajv it does.
105
+ *
106
+ * Requires `validators`. `validateX` and `isX` are unchanged either way.
107
+ */
108
+ readonly coerce?: boolean;
109
+ /**
110
+ * When true, generated validators also get a `repairX`: it coerces, validates,
111
+ * and then repairs each position the validator rejected to a value the schema
112
+ * itself supplies — a `default`, a `const`, the first `enum` member, or a
113
+ * fallback built to satisfy the position's own bounds. The result carries the
114
+ * repaired value together with `repairs`, which *are* the errors `validateX`
115
+ * produced for the positions that were repaired, so a caller logs the same
116
+ * path, keyword and params it would have been rejected with.
117
+ *
118
+ * A document needing no repair comes back valid with an empty `repairs`; one
119
+ * fully repaired comes back valid with a non-empty one, leaving the caller to
120
+ * decide whether that is acceptable; one that could not be fully repaired
121
+ * comes back invalid carrying both the repairs applied and the errors still
122
+ * outstanding.
123
+ *
124
+ * Implies `coerce` — repairing runs after coercion, so a value merely written
125
+ * in the wrong type is right before the validator sees it and never counts as
126
+ * a repair. Requires `validators`.
127
+ */
128
+ readonly repair?: boolean;
129
+ /**
130
+ * When true, a failing `anyOf` / `oneOf` in a generated validator also reports
131
+ * the errors of the branch it meant, instead of the bare "must match a schema
132
+ * in anyOf" that names no field and no reason.
133
+ *
134
+ * Off by default, and off costs nothing — the generated code is exactly what
135
+ * it would be without the option. On, each branch keeps what it complained
136
+ * about, which is a push per *failing* branch and one lazily-created array per
137
+ * combinator that had one. A valid instance fails no branch and allocates
138
+ * nothing, but the branches are still handed a collector, so measure before
139
+ * turning it on in a hot path.
140
+ *
141
+ * Requires `validators`.
142
+ */
143
+ readonly branchErrors?: boolean;
54
144
  /**
55
145
  * When true, also emit test-data files for every schema: a `fast-check`
56
146
  * arbitrary (`FooArbitrary`) that produces schema-valid values and a concrete
package/dist/cli.js CHANGED
@@ -4,8 +4,6 @@ import { readdir, unlink, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { extractAsyncApi, listMessageSchemas } from "@amritk/asyncapi";
7
- import { buildSchema } from "@amritk/generate-parsers";
8
- import { buildValidatorSchema } from "@amritk/generate-validators";
9
7
  import { deriveRootTypeName } from "@amritk/helpers/derive-root-type-name";
10
8
  const execFileAsync = promisify(execFile);
11
9
  import { combineGeneratedFiles } from "./combine-files.js";
@@ -14,6 +12,7 @@ import { detectHelpersMode } from "./detect-helpers-mode.js";
14
12
  import { emitExamples } from "./emit-examples.js";
15
13
  import { CONTRACTS_DIR, CONTRACTS_PEER, emitMessageContracts } from "./emit-message-contracts.js";
16
14
  import { ensureOutputDir } from "./ensure-output-dir.js";
15
+ import { generateFiles } from "./generate-files.js";
17
16
  import { HELP_TEXT } from "./help-text.js";
18
17
  import { isDependencyDeclared } from "./is-dependency-declared.js";
19
18
  import { loadAsyncApiDocument } from "./load-asyncapi-document.js";
@@ -22,7 +21,6 @@ import { loadSchema } from "./load-schema.js";
22
21
  import { parseCliArgs } from "./parse-cli-args.js";
23
22
  import { parseMetaRequest } from "./parse-meta-request.js";
24
23
  import { readVersion } from "./read-version.js";
25
- import { resolveImportExt } from "./resolve-import-ext.js";
26
24
  const ROOT_TYPE_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
27
25
  const DISCRIMINATOR_PATTERN = /^[A-Za-z_$][\w$-]*$/;
28
26
  const schemaBaseName = (schemaPath) => basename(schemaPath).replace(/\.[^.]+$/, "");
@@ -138,24 +136,13 @@ const findJsonSchemas = async (dir) => {
138
136
  }
139
137
  return results;
140
138
  };
141
- const runValidators = async (config, schema, rootTypeName, writer, validatorsSubDir) => {
142
- const files = await buildValidatorSchema(schema, rootTypeName, config.typeSuffix, void 0, config.unknownKeys, config.formats);
143
- const staged = [];
144
- for (const file of files) {
145
- const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
146
- const relFilename = join(validatorsSubDir, file.filename);
147
- await writer.stage(relFilename, content);
148
- staged.push(relFilename);
149
- }
150
- return staged;
151
- };
152
139
  const runSingle = async (config, schemaPath, outputDir) => {
153
140
  const schema = await loadSchema(config, schemaPath);
154
141
  await ensureOutputDir(outputDir);
155
142
  const helpersMode = resolveHelpersMode(config, outputDir);
156
143
  const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
157
144
  console.log(`Root type: ${rootTypeName}`);
158
- const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
145
+ const files = await generateFiles(config, schema, rootTypeName, { mode: helpersMode });
159
146
  const writer = await createOutputWriter(outputDir);
160
147
  const written = await commitOrDiscard(writer, async () => {
161
148
  for (const file of files) {
@@ -163,8 +150,6 @@ const runSingle = async (config, schemaPath, outputDir) => {
163
150
  const content = config.banner && !isHelper ? resolveBanner(config.banner) + file.content : file.content;
164
151
  await writer.stage(file.filename, content);
165
152
  }
166
- if (config.validators)
167
- await runValidators(config, schema, rootTypeName, writer, "validators");
168
153
  });
169
154
  for (const filename of written)
170
155
  console.log(`Generated: ${filename}`);
@@ -184,7 +169,9 @@ const runSingleFile = async (config, schemaPath, outFilePath) => {
184
169
  await ensureOutputDir(outputDir);
185
170
  const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
186
171
  console.log(`Root type: ${rootTypeName}`);
187
- const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? "package", void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
172
+ const files = await generateFiles(config, schema, rootTypeName, {
173
+ mode: config.helpers ?? "package"
174
+ });
188
175
  const combined = combineGeneratedFiles(files);
189
176
  const writer = await createOutputWriter(outputDir);
190
177
  const written = await commitOrDiscard(writer, async () => {
@@ -226,7 +213,10 @@ const runRecursive = async (config, schemaDir, outputDir) => {
226
213
  const rootTypeName = deriveRootTypeName(schema, schemaBaseName(schemaFile));
227
214
  console.log(`
228
215
  ${relPath} \u2192 ${relNoExt}/ (root type: ${rootTypeName})`);
229
- const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
216
+ const files = await generateFiles(config, schema, rootTypeName, {
217
+ mode: helpersMode,
218
+ importPrefix: helpersImportPrefix
219
+ });
230
220
  for (const file of files) {
231
221
  if (file.filename.startsWith("_helpers/")) {
232
222
  sharedHelpers.set(file.filename, file.content);
@@ -235,9 +225,6 @@ ${relPath} \u2192 ${relNoExt}/ (root type: ${rootTypeName})`);
235
225
  const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
236
226
  await writer.stage(join(relNoExt, file.filename), content);
237
227
  }
238
- if (config.validators) {
239
- await runValidators(config, schema, rootTypeName, writer, join("validators", relNoExt));
240
- }
241
228
  if (config.examples)
242
229
  exampleTasks.push({ schema, rootTypeName, subDir: relNoExt });
243
230
  }
@@ -291,7 +278,10 @@ const runAsyncApi = async (config, documentPath, outputDir) => {
291
278
  const helpersImportPrefix = "../".repeat(depth);
292
279
  console.log(`
293
280
  ${subDir}/ (root type: ${rootTypeName})`);
294
- const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
281
+ const files = await generateFiles(config, schema, rootTypeName, {
282
+ mode: helpersMode,
283
+ importPrefix: helpersImportPrefix
284
+ });
295
285
  for (const file of files) {
296
286
  if (file.filename.startsWith("_helpers/")) {
297
287
  sharedHelpers.set(file.filename, file.content);
@@ -300,9 +290,6 @@ ${subDir}/ (root type: ${rootTypeName})`);
300
290
  const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
301
291
  await writer.stage(join(subDir, file.filename), content);
302
292
  }
303
- if (config.validators) {
304
- await runValidators(config, schema, rootTypeName, writer, join("validators", subDir));
305
- }
306
293
  if (config.examples)
307
294
  exampleTasks.push({ schema, rootTypeName, subDir });
308
295
  }
@@ -390,13 +377,27 @@ const run = async () => {
390
377
  const fileConfig = configPath ? await loadConfig(configPath) : {};
391
378
  const cliConfig = parseCliArgs(args);
392
379
  const config = { ...fileConfig, ...cliConfig };
380
+ const wantsValidators = config.validators === true || config.validatorsOnly === true;
381
+ if (config.coerce && !wantsValidators) {
382
+ console.error("Error: --coerce shapes the generated validators, so it needs --validators too.");
383
+ process.exit(1);
384
+ }
385
+ if (config.repair && !wantsValidators) {
386
+ console.error("Error: --repair shapes the generated validators, so it needs --validators too.");
387
+ process.exit(1);
388
+ }
389
+ if (config.check && !wantsValidators) {
390
+ console.error("Error: --check shapes the generated validators, so it needs --validators too.");
391
+ process.exit(1);
392
+ }
393
+ if (config.branchErrors && !wantsValidators) {
394
+ console.error("Error: --branch-errors shapes the generated validators, so it needs --validators too.");
395
+ process.exit(1);
396
+ }
393
397
  if (config.outDir && config.outFile) {
394
398
  console.error("Error: provide only one of --out-dir or --out-file, not both.");
395
399
  process.exit(1);
396
400
  }
397
- if (config.force) {
398
- console.warn("Warning: --force is deprecated and does nothing; generated files always replace what is at their path.");
399
- }
400
401
  if (config.rootType !== void 0 && !ROOT_TYPE_PATTERN.test(config.rootType)) {
401
402
  console.error(`Error: invalid --root-type "${config.rootType}". Expected a TypeScript identifier (letters, digits, _ or $, not starting with a digit).`);
402
403
  process.exit(1);
@@ -413,10 +414,18 @@ const run = async () => {
413
414
  console.error("Error: --validators cannot be combined with --types-only, which emits no runtime code.");
414
415
  process.exit(1);
415
416
  }
417
+ if (config.validatorsOnly && config.typesOnly) {
418
+ console.error("Error: --validators-only cannot be combined with --types-only. Pick the one end you want: types alone, or types and validators.");
419
+ process.exit(1);
420
+ }
416
421
  if (config.validators && config.outFile) {
417
422
  console.error("Error: --validators cannot be combined with --out-file. Use --out-dir to emit validator files.");
418
423
  process.exit(1);
419
424
  }
425
+ if (config.validatorsOnly && config.outFile) {
426
+ console.error("Error: --validators-only cannot be combined with --out-file. Use --out-dir to emit validator files.");
427
+ process.exit(1);
428
+ }
420
429
  if (config.input === "asyncapi") {
421
430
  if (config.schemaDir) {
422
431
  console.error("Error: --input asyncapi reads a single document via --schema; it cannot be combined with --schema-dir.");
@@ -1,4 +1,4 @@
1
- import type { GeneratedFile } from '@amritk/generate-parsers';
1
+ import type { GeneratedFile } from '@amritk/validation';
2
2
  /**
3
3
  * Merges the per-schema {@link GeneratedFile}s into a single self-contained
4
4
  * TypeScript module. Each generated file imports its sibling definitions via
@@ -0,0 +1,24 @@
1
+ import { type GeneratedFile } from '@amritk/validation';
2
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
3
+ import type { CliConfig } from './cli-config.js';
4
+ /** Where one schema's generated files reach their runtime helpers from. */
5
+ export type HelpersPlacement = {
6
+ /** The published package, or helper sources emitted into the output tree. */
7
+ readonly mode: 'package' | 'embedded';
8
+ /**
9
+ * Relative path from this schema's output subdirectory back to the shared
10
+ * `_helpers/` at the output root. The recursive flows nest a schema's files a
11
+ * directory or more down, so `./` is only right at the top.
12
+ */
13
+ readonly importPrefix?: string;
14
+ };
15
+ /**
16
+ * Generates one schema's files, translating the CLI's flags into the single
17
+ * options object `@amritk/validation` takes.
18
+ *
19
+ * All four generation flows come through here so none of them can quietly hold a
20
+ * different opinion about what a flag means — which is exactly what happened
21
+ * while the CLI drove the two generators directly and positionally: the AsyncAPI
22
+ * flow passed `--unknown-keys` to the validators and forgot it for the parsers.
23
+ */
24
+ export declare const generateFiles: (config: Partial<CliConfig>, schema: JSONSchema, rootTypeName: string, helpers: HelpersPlacement) => Promise<GeneratedFile[]>;
@@ -0,0 +1,24 @@
1
+ import { generate } from "@amritk/validation";
2
+ import { resolveImportExt } from "./resolve-import-ext.js";
3
+ import { resolveModes } from "./resolve-modes.js";
4
+ const generateFiles = async (config, schema, rootTypeName, helpers) => generate(schema, rootTypeName, {
5
+ modes: resolveModes(config),
6
+ typeSuffix: config.typeSuffix ?? "",
7
+ branchErrors: config.branchErrors === true,
8
+ stripUnknown: config.stripUnknown === true,
9
+ readonly: config.readonly === true,
10
+ caseInsensitive: config.caseInsensitive === true,
11
+ helpersMode: helpers.mode,
12
+ helpersImportPrefix: helpers.importPrefix ?? "./",
13
+ importExt: resolveImportExt(config),
14
+ logWarnings: config.logWarnings === true,
15
+ // Spread rather than passed as `undefined`: under
16
+ // `exactOptionalPropertyTypes` an absent option and one set to `undefined`
17
+ // are different things, and only the first falls back to the generator's own
18
+ // default.
19
+ ...config.unknownKeys !== void 0 ? { unknownKeys: config.unknownKeys } : {},
20
+ ...config.formats !== void 0 ? { formats: config.formats } : {}
21
+ });
22
+ export {
23
+ generateFiles
24
+ };
@@ -4,4 +4,4 @@
4
4
  * documents these flags (config.schema.json) is not shipped in the published
5
5
  * package; the help-text test cross-checks that every flag is listed.
6
6
  */
7
- export declare const HELP_TEXT = "mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nSubcommands (each with its own flags and --help):\n mjst lint <files> Lint JSON/YAML documents against a ruleset\n mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module\n mjst markdown <schema> --out-dir <dir> Generate markdown documentation from a JSON Schema\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect, avro,\n or asyncapi (an AsyncAPI 2.x/3.0 document, JSON or YAML)\n --export <name> Which export of the schema module to use when --input names a module format\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --validators Also emit validateX/isX validators into a validators/ subdir\n --examples Also emit fast-check arbitraries + example values under examples/\n --message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)\n --discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force Deprecated; ignored (generated files always overwrite)\n --import-ext <ext> Extension on generated relative imports: ts (default) or js\n --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)\n --root-type <name> Root type name for a single --schema run (default: schema title or filename)\n --type-suffix <s> Suffix appended to every $ref-derived type name\n --banner [text] Prepend a header comment to every generated file\n --readonly Emit deeply readonly type definitions\n\nValidation:\n --strict Throw on type/shape mismatches instead of coercing to defaults\n --strip-unknown Silently drop undeclared input keys at every nesting level\n --case-insensitive Normalize mis-cased enum/const strings to their exact casing\n --log-warnings console.warn on input keys not declared in the schema\n --unknown-keys <s> How closed objects are proven free of undeclared keys:\n count-keys (default, fastest on Bun) or count-enumerable (fastest on Node)\n --formats <f> String formats the generated validators enforce: \"all\", or a\n comma-separated list (e.g. uuid,date-time,email). Off by default,\n which leaves \"format\" an annotation as JSON Schema reads it.\n\nReference resolution:\n --resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)\n --allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)\n --allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)\n --allowed-roots <d> Extra directories a local $ref may resolve into, on top of the schema's own\n (comma-separated; relative to the current directory)\n\nMisc:\n --config <path> JSON config file with the same keys; CLI flags take precedence\n --version, -v Print the CLI version\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
7
+ export declare const HELP_TEXT = "mjst \u2014 generate TypeScript types, validators and parsers from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nSubcommands (each with its own flags and --help):\n mjst lint <files> Lint JSON/YAML documents against a ruleset\n mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module\n mjst markdown <schema> --out-dir <dir> Generate markdown documentation from a JSON Schema\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect, avro,\n or asyncapi (an AsyncAPI 2.x/3.0 document, JSON or YAML)\n --export <name> Which export of the schema module to use when --input names a module format\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --validators Also emit validateX/isX validators beside the parsers, over one\n shared declaration of each generated type\n --validators-only Emit the validator half and no parser: the type, isX, validateX and\n whichever of checkX/coerceX/repairX were asked for (implies\n --validators; the mirror of --types-only)\n --check Also emit checkX, the same result as validateX but stopped at the\n first violation (requires --validators)\n --coerce Also emit coerceX, which coerces scalars the way Ajv's coerceTypes\n does and then validates (requires --validators)\n --repair Also emit repairX, which coerces, validates, then repairs each\n rejected position to a schema-supplied value and reports the\n errors it repaired (implies --coerce; requires --validators)\n --branch-errors Explain a failing anyOf/oneOf with the errors of the branch it\n meant (requires --validators; costs a little on unions)\n --examples Also emit fast-check arbitraries + example values under examples/\n --message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)\n --discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force Deprecated; ignored (generated files always overwrite)\n --import-ext <ext> Extension on generated relative imports: ts (default) or js\n --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)\n --root-type <name> Root type name for a single --schema run (default: schema title or filename)\n --type-suffix <s> Suffix appended to every $ref-derived type name\n --banner [text] Prepend a header comment to every generated file\n --readonly Emit deeply readonly type definitions\n\nValidation:\n --strict Throw on type/shape mismatches instead of coercing to defaults\n --strip-unknown Silently drop undeclared input keys at every nesting level\n --case-insensitive Normalize mis-cased enum/const strings to their exact casing\n --log-warnings console.warn on input keys not declared in the schema\n --unknown-keys <s> How closed objects are proven free of undeclared keys:\n count-keys (default, fastest on Bun) or count-enumerable (fastest on Node)\n --formats <f> String formats the generated validators enforce: \"all\", or a\n comma-separated list (e.g. uuid,date-time,email). Off by default,\n which leaves \"format\" an annotation as JSON Schema reads it.\n\nReference resolution:\n --resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)\n --allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)\n --allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)\n --allowed-roots <d> Extra directories a local $ref may resolve into, on top of the schema's own\n (comma-separated; relative to the current directory)\n\nMisc:\n --config <path> JSON config file with the same keys; CLI flags take precedence\n --version, -v Print the CLI version\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
package/dist/help-text.js CHANGED
@@ -1,4 +1,4 @@
1
- const HELP_TEXT = `mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas
1
+ const HELP_TEXT = `mjst \u2014 generate TypeScript types, validators and parsers from JSON Schemas
2
2
 
3
3
  Usage:
4
4
  mjst --schema <path> --out-dir <dir> [options]
@@ -21,7 +21,20 @@ Output:
21
21
  --out-dir <dir> Output directory for the generated files
22
22
  --out-file <file> Single-file output instead of a directory (requires --types-only)
23
23
  --types-only Generate type definitions only, without parser functions
24
- --validators Also emit validateX/isX validators into a validators/ subdir
24
+ --validators Also emit validateX/isX validators beside the parsers, over one
25
+ shared declaration of each generated type
26
+ --validators-only Emit the validator half and no parser: the type, isX, validateX and
27
+ whichever of checkX/coerceX/repairX were asked for (implies
28
+ --validators; the mirror of --types-only)
29
+ --check Also emit checkX, the same result as validateX but stopped at the
30
+ first violation (requires --validators)
31
+ --coerce Also emit coerceX, which coerces scalars the way Ajv's coerceTypes
32
+ does and then validates (requires --validators)
33
+ --repair Also emit repairX, which coerces, validates, then repairs each
34
+ rejected position to a schema-supplied value and reports the
35
+ errors it repaired (implies --coerce; requires --validators)
36
+ --branch-errors Explain a failing anyOf/oneOf with the errors of the branch it
37
+ meant (requires --validators; costs a little on unions)
25
38
  --examples Also emit fast-check arbitraries + example values under examples/
26
39
  --message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)
27
40
  --discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)
@@ -30,10 +30,13 @@ const loadConfig = async (configPath) => {
30
30
  ...typeof obj["export"] === "string" && { export: obj["export"] },
31
31
  ...typeof obj["validators"] === "boolean" && { validators: obj["validators"] },
32
32
  ...typeof obj["typesOnly"] === "boolean" && { typesOnly: obj["typesOnly"] },
33
+ ...typeof obj["validatorsOnly"] === "boolean" && { validatorsOnly: obj["validatorsOnly"] },
33
34
  ...typeof obj["examples"] === "boolean" && { examples: obj["examples"] },
34
35
  ...typeof obj["messageContracts"] === "boolean" && { messageContracts: obj["messageContracts"] },
35
36
  ...typeof obj["discriminator"] === "string" && { discriminator: obj["discriminator"] },
36
37
  ...typeof obj["build"] === "boolean" && { build: obj["build"] },
38
+ ...typeof obj["coerce"] === "boolean" && { coerce: obj["coerce"] },
39
+ ...typeof obj["branchErrors"] === "boolean" && { branchErrors: obj["branchErrors"] },
37
40
  ...typeof obj["force"] === "boolean" && { force: obj["force"] },
38
41
  ...typeof obj["logWarnings"] === "boolean" && { logWarnings: obj["logWarnings"] },
39
42
  ...typeof obj["strict"] === "boolean" && { strict: obj["strict"] },
@@ -3,4 +3,4 @@
3
3
  * HELP_TEXT: the flag list is small and stable, and the test cross-checks that
4
4
  * every flag is listed.
5
5
  */
6
- export declare const MARKDOWN_HELP_TEXT = "mjst markdown \u2014 generate markdown documentation from a JSON Schema\n\nUsage:\n mjst markdown <schema> --out-dir <dir> [options]\n mjst markdown <schema> --table --readme <file>\n\nTwo shapes come out of the same schema. By default it writes the prose\nreference \u2014 a heading, a type, the description and an example per property,\nacross as many pages as the schema's x-doc keyword declares. With --table it\nrenders one HTML table instead and splices it into an existing markdown file\nbetween <!-- config-table-start --> and <!-- config-table-end -->, leaving the\nrest of that file alone.\n\nProse reference options:\n --out-dir <dir> Directory the pages are written to (default: the current directory)\n --file <path> Output path of the index page (default: the schema's x-doc.file, then index.md)\n --title <text> Page title (default: the schema's title)\n --language <lang> Fence language for derived examples and literals (default: json)\n --layout <mode> Default layout for nested properties: headings (default), table, or none\n --sort <order> Property order: schema (default) or alphabetical\n --heading-level <n> Heading level of the page title, 1-6 (default: 1)\n\nTable options:\n --table Render the HTML config table instead of the prose pages\n --readme <path> Markdown file the table is spliced into (default: README.md)\n\nMisc:\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
6
+ export declare const MARKDOWN_HELP_TEXT = "mjst markdown \u2014 generate markdown documentation from a JSON Schema\n\nUsage:\n mjst markdown <schema> --out-dir <dir> [options]\n mjst markdown <schema> --table --readme <file>\n\nTwo shapes come out of the same schema. By default it writes the prose\nreference \u2014 a heading, a type, the description and an example per property,\nacross as many pages as the schema's x-doc keyword declares. With --table it\nrenders one HTML table instead and splices it into an existing markdown file\nbetween <!-- config-table-start --> and <!-- config-table-end -->, leaving the\nrest of that file alone.\n\nProse reference options:\n --out-dir <dir> Directory the pages are written to (default: the current directory)\n --file <path> Output path of the index page (default: the schema's x-doc.file, then index.md)\n --title <text> Page title (default: the schema's title)\n --language <lang> Fence language for derived examples and literals (default: json)\n --layout <mode> Default layout for nested properties: headings (default), table, or none\n --sort <order> Property order: schema (default) or alphabetical\n --heading-level <n> Heading level of the page title, 1-6 (default: 1)\n\nProperty table options (the root x-doc.table, for every table on every page):\n --type-column <when> Render the Type column: auto (default), always, or never\n --default-column <when> Render the Default column: auto (default), always, or never\n --required-style <how> Say which properties are required: marker (default, beside\n the name) or column (a Required column)\n --required-first List the required properties at the top of every table\n\nTable options:\n --table Render the HTML config table instead of the prose pages\n --readme <path> Markdown file the table is spliced into (default: README.md)\n\nMisc:\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
@@ -20,6 +20,13 @@ Prose reference options:
20
20
  --sort <order> Property order: schema (default) or alphabetical
21
21
  --heading-level <n> Heading level of the page title, 1-6 (default: 1)
22
22
 
23
+ Property table options (the root x-doc.table, for every table on every page):
24
+ --type-column <when> Render the Type column: auto (default), always, or never
25
+ --default-column <when> Render the Default column: auto (default), always, or never
26
+ --required-style <how> Say which properties are required: marker (default, beside
27
+ the name) or column (a Required column)
28
+ --required-first List the required properties at the top of every table
29
+
23
30
  Table options:
24
31
  --table Render the HTML config table instead of the prose pages
25
32
  --readme <path> Markdown file the table is spliced into (default: README.md)
@@ -1,4 +1,4 @@
1
- import type { DocLayout, DocSort } from '@amritk/generate-markdown';
1
+ import type { DocLayout, DocSort, DocTableColumn, DocTableRequired } from '@amritk/generate-markdown';
2
2
  /** The flags and positional the `markdown` subcommand understands. */
3
3
  export type MarkdownArgs = {
4
4
  /** Path to the JSON Schema to document (the sole positional). */
@@ -17,6 +17,14 @@ export type MarkdownArgs = {
17
17
  sort?: DocSort;
18
18
  /** Heading level of the page title (`--heading-level`). */
19
19
  headingLevel?: number;
20
+ /** When property tables render their Type column (`--type-column`). */
21
+ typeColumn?: DocTableColumn;
22
+ /** When property tables render their Default column (`--default-column`). */
23
+ defaultColumn?: DocTableColumn;
24
+ /** How property tables say which properties are required (`--required-style`). */
25
+ requiredStyle?: DocTableRequired;
26
+ /** True when `--required-first` was passed: required properties head every table. */
27
+ requiredFirst?: boolean;
20
28
  /** True when `--table` was passed: render the HTML table instead of the pages. */
21
29
  table?: boolean;
22
30
  /** Markdown file the table is spliced into (`--readme`). */
@@ -1,7 +1,21 @@
1
- const VALUE_KEYS = /* @__PURE__ */ new Set(["outDir", "file", "title", "language", "layout", "sort", "headingLevel", "readme"]);
2
- const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["table"]);
1
+ const VALUE_KEYS = /* @__PURE__ */ new Set([
2
+ "outDir",
3
+ "file",
4
+ "title",
5
+ "language",
6
+ "layout",
7
+ "sort",
8
+ "headingLevel",
9
+ "typeColumn",
10
+ "defaultColumn",
11
+ "requiredStyle",
12
+ "readme"
13
+ ]);
14
+ const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["table", "requiredFirst"]);
3
15
  const LAYOUTS = ["headings", "table", "none"];
4
16
  const SORTS = ["schema", "alphabetical"];
17
+ const TABLE_COLUMNS = ["auto", "always", "never"];
18
+ const REQUIRED_STYLES = ["marker", "column"];
5
19
  const toCamelCase = (key) => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6
20
  const parseChoice = (flag, value, choices) => {
7
21
  const match = choices.find((choice) => choice === value);
@@ -40,6 +54,15 @@ const assignValue = (args, key, value) => {
40
54
  case "headingLevel":
41
55
  args.headingLevel = parseHeadingLevel(value);
42
56
  return;
57
+ case "typeColumn":
58
+ args.typeColumn = parseChoice("type-column", value, TABLE_COLUMNS);
59
+ return;
60
+ case "defaultColumn":
61
+ args.defaultColumn = parseChoice("default-column", value, TABLE_COLUMNS);
62
+ return;
63
+ case "requiredStyle":
64
+ args.requiredStyle = parseChoice("required-style", value, REQUIRED_STYLES);
65
+ return;
43
66
  case "readme":
44
67
  args.readme = value;
45
68
  return;
@@ -78,7 +101,10 @@ const parseMarkdownArgs = (argv) => {
78
101
  const flagName = arg.slice(2);
79
102
  const key = toCamelCase(flagName);
80
103
  if (BOOLEAN_KEYS.has(key)) {
81
- args.table = true;
104
+ if (key === "requiredFirst")
105
+ args.requiredFirst = true;
106
+ else
107
+ args.table = true;
82
108
  continue;
83
109
  }
84
110
  if (!VALUE_KEYS.has(key))
@@ -6,7 +6,19 @@ const usageError = (message) => ({ code: 2, stdout: "", stderr: `Error: ${messag
6
6
  ` });
7
7
  const failure = (message) => ({ code: 1, stdout: "", stderr: `Error: ${message}
8
8
  ` });
9
- const PAGE_FLAGS = ["outDir", "file", "title", "language", "layout", "sort", "headingLevel"];
9
+ const PAGE_FLAGS = [
10
+ "outDir",
11
+ "file",
12
+ "title",
13
+ "language",
14
+ "layout",
15
+ "sort",
16
+ "headingLevel",
17
+ "typeColumn",
18
+ "defaultColumn",
19
+ "requiredStyle",
20
+ "requiredFirst"
21
+ ];
10
22
  const run = async (argv) => {
11
23
  let args;
12
24
  try {
@@ -51,7 +63,15 @@ const run = async (argv) => {
51
63
  language: args.language,
52
64
  layout: args.layout,
53
65
  sort: args.sort,
54
- headingLevel: args.headingLevel
66
+ headingLevel: args.headingLevel,
67
+ // Always passed: every member is optional, so the schema's own
68
+ // `x-doc.table` still decides whatever the flags left alone.
69
+ table: {
70
+ type: args.typeColumn,
71
+ default: args.defaultColumn,
72
+ required: args.requiredStyle,
73
+ requiredFirst: args.requiredFirst
74
+ }
55
75
  };
56
76
  let files;
57
77
  try {
@@ -4,6 +4,11 @@ const LIST_KEYS = /* @__PURE__ */ new Set(["allowedHosts", "allowedRoots"]);
4
4
  const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
5
5
  "typesOnly",
6
6
  "validators",
7
+ "validatorsOnly",
8
+ "check",
9
+ "coerce",
10
+ "repair",
11
+ "branchErrors",
7
12
  "examples",
8
13
  "messageContracts",
9
14
  "build",
@@ -124,9 +129,24 @@ const assignBoolean = (config, key, value) => {
124
129
  case "typesOnly":
125
130
  config.typesOnly = value;
126
131
  return true;
132
+ case "validatorsOnly":
133
+ config.validatorsOnly = value;
134
+ return true;
127
135
  case "validators":
128
136
  config.validators = value;
129
137
  return true;
138
+ case "check":
139
+ config.check = value;
140
+ return true;
141
+ case "coerce":
142
+ config.coerce = value;
143
+ return true;
144
+ case "repair":
145
+ config.repair = value;
146
+ return true;
147
+ case "branchErrors":
148
+ config.branchErrors = value;
149
+ return true;
130
150
  case "examples":
131
151
  config.examples = value;
132
152
  return true;
@@ -0,0 +1,19 @@
1
+ import type { Mode } from '@amritk/validation';
2
+ import type { CliConfig } from './cli-config.js';
3
+ /**
4
+ * Turns the flags a run was given into the list of entry points `generate()`
5
+ * should emit.
6
+ *
7
+ * The CLI predates the mode list: its flags grew one at a time, each one meaning
8
+ * "also emit this function", so the mapping is additive rather than a choice.
9
+ * Keeping it in one place means the four generation flows cannot drift on what
10
+ * `--strict --validators --repair` is supposed to produce.
11
+ *
12
+ * `--types-only` is the one run with nothing to execute, so it stops at `types`
13
+ * — asking for a parser there would emit runtime code the flag exists to avoid.
14
+ * `--validators-only` is its mirror at the other end: everything that judges a
15
+ * document, and nothing that builds one. Between them, every run carries a
16
+ * parser, because emitting one is what the CLI does when you do not tell it
17
+ * otherwise.
18
+ */
19
+ export declare const resolveModes: (config: Partial<CliConfig>) => Mode[];
@@ -0,0 +1,21 @@
1
+ const resolveModes = (config) => {
2
+ if (config.typesOnly === true)
3
+ return ["types"];
4
+ const modes = ["types"];
5
+ if (config.validators === true || config.validatorsOnly === true) {
6
+ modes.push("guard", "validate");
7
+ if (config.check === true)
8
+ modes.push("check");
9
+ if (config.coerce === true)
10
+ modes.push("coerce");
11
+ if (config.repair === true)
12
+ modes.push("repair");
13
+ }
14
+ if (config.validatorsOnly !== true) {
15
+ modes.push(config.strict === true ? "parseStrict" : "parse");
16
+ }
17
+ return modes;
18
+ };
19
+ export {
20
+ resolveModes
21
+ };
@@ -8,7 +8,12 @@ const CONFIG_KEYS = {
8
8
  outDir: { type: "string" },
9
9
  outFile: { type: "string" },
10
10
  typesOnly: { type: "boolean" },
11
+ validatorsOnly: { type: "boolean" },
11
12
  validators: { type: "boolean" },
13
+ check: { type: "boolean" },
14
+ coerce: { type: "boolean" },
15
+ repair: { type: "boolean" },
16
+ branchErrors: { type: "boolean" },
12
17
  examples: { type: "boolean" },
13
18
  messageContracts: { type: "boolean" },
14
19
  discriminator: { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/mjst",
3
- "version": "0.21.1",
3
+ "version": "0.23.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -49,15 +49,14 @@
49
49
  "./package.json": "./package.json"
50
50
  },
51
51
  "dependencies": {
52
- "@amritk/adapters": "^0.6.3",
53
- "@amritk/api": "^0.16.4",
54
- "@amritk/asyncapi": "^0.3.1",
55
- "@amritk/generate-examples": "^0.8.5",
56
- "@amritk/generate-markdown": "^0.8.0",
57
- "@amritk/generate-parsers": "^0.24.0",
58
- "@amritk/generate-validators": "^0.18.0",
59
- "@amritk/helpers": "^0.22.0",
60
- "@amritk/lint": "^0.6.0",
52
+ "@amritk/adapters": "^0.6.5",
53
+ "@amritk/api": "^0.16.5",
54
+ "@amritk/asyncapi": "^0.3.3",
55
+ "@amritk/generate-examples": "^0.8.7",
56
+ "@amritk/generate-markdown": "^0.9.0",
57
+ "@amritk/helpers": "^0.23.1",
58
+ "@amritk/lint": "^0.6.1",
59
+ "@amritk/validation": "^0.3.0",
61
60
  "@amritk/resolve-refs": "^0.7.1",
62
61
  "@amritk/yaml": "^0.7.2",
63
62
  "fast-glob": "^3.3.2",