@amritk/mjst 0.21.0 → 0.22.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/parsers` etc. instead.
12
12
 
13
13
  ## The four commands
14
14
 
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
 
@@ -389,7 +405,43 @@ Under `--table` only the content between `<!-- config-table-start -->
389
405
  <td align="center"><code>false</code></td>
390
406
  </tr>
391
407
  <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>
408
+ <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>
409
+ </tr>
410
+ <tr>
411
+ <td>🔎 <code>check</code></td>
412
+ <td><code>--check</code></td>
413
+ <td><code>boolean</code></td>
414
+ <td align="center"><code>false</code></td>
415
+ </tr>
416
+ <tr>
417
+ <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>
418
+ </tr>
419
+ <tr>
420
+ <td>🧪 <code>coerce</code></td>
421
+ <td><code>--coerce</code></td>
422
+ <td><code>boolean</code></td>
423
+ <td align="center"><code>false</code></td>
424
+ </tr>
425
+ <tr>
426
+ <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>
427
+ </tr>
428
+ <tr>
429
+ <td>🔧 <code>repair</code></td>
430
+ <td><code>--repair</code></td>
431
+ <td><code>boolean</code></td>
432
+ <td align="center"><code>false</code></td>
433
+ </tr>
434
+ <tr>
435
+ <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>
436
+ </tr>
437
+ <tr>
438
+ <td>🌿 <code>branchErrors</code></td>
439
+ <td><code>--branch-errors</code></td>
440
+ <td><code>boolean</code></td>
441
+ <td align="center"><code>false</code></td>
442
+ </tr>
443
+ <tr>
444
+ <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
445
  </tr>
394
446
  <tr>
395
447
  <td>🎲 <code>examples</code></td>
@@ -686,7 +738,7 @@ usage error. Run `mjst markdown --help` for details.
686
738
  <td align="center"><code>false</code></td>
687
739
  </tr>
688
740
  <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>
741
+ <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
742
  </tr>
691
743
  <tr>
692
744
  <td>🎲 <code>examples</code></td>
@@ -45,12 +45,91 @@ export type CliConfig = {
45
45
  * When true, also emit validation functions alongside the parsers. For every
46
46
  * generated type `X` the CLI writes a `validateX` (returning a rich
47
47
  * `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.
48
+ * guard.
49
+ *
50
+ * They share one directory and one declaration of `X` with the parser: the
51
+ * type and the validators land in `x.ts`, and the parser moves to `x.parse.ts`
52
+ * beside them, importing the type from there. Both halves are then talking
53
+ * about the same type, which two side-by-side trees could only promise.
54
+ *
55
+ * Works with both `schema` and `schemaDir`. Incompatible with `typesOnly` and
56
+ * `outFile`, which produce no runtime code.
52
57
  */
53
58
  readonly validators?: boolean;
59
+ /**
60
+ * When true, generated validators also get a `checkX`: the same
61
+ * `ValidationResult` `validateX` returns, carrying the one error that stopped
62
+ * it. It gives up at the first violation instead of walking on to collect the
63
+ * rest, so it costs a single error object where `validateX` costs however many
64
+ * the document earns.
65
+ *
66
+ * Reach for it when a failure has to be reported but only the first thing
67
+ * wrong matters — a service refusing to boot on a bad config does not need the
68
+ * other nine. When nothing has to be reported at all, `isX` is cheaper still:
69
+ * it builds no error object.
70
+ *
71
+ * Requires `validators`. `validateX` and `isX` are unchanged either way.
72
+ */
73
+ readonly check?: boolean;
74
+ /**
75
+ * When true, generated validators also get a `coerceX`: it moves scalars
76
+ * toward what the schema asks for and then runs the very same `validateX` over
77
+ * the result, so a config value written the YAML way reads the same however it
78
+ * arrived.
79
+ *
80
+ * The input is never modified, and nothing is ever substituted: where a value
81
+ * cannot be coerced into a valid one it is handed to the validator untouched,
82
+ * so the error names what the caller actually wrote, with the keyword and
83
+ * params that rejected it. That is the difference from a coercing *parser*,
84
+ * which repairs toward a default and leaves nothing to report.
85
+ *
86
+ * The table is Ajv's `coerceTypes` minus the cells where Ajv guesses — no
87
+ * whitespace-to-zero, no `0x`/`Infinity` strings, and nothing coerced to or
88
+ * from `null`. Every value this coerces, Ajv coerces to the same value, so
89
+ * moving off Ajv never changes a value; it turns some of Ajv's silent repairs
90
+ * into errors instead. At a union it coerces only when exactly one of the
91
+ * offered types can take the value, and leaves a value that is already one of
92
+ * them alone — so the answer does not depend on the order the union is
93
+ * written in, which under Ajv it does.
94
+ *
95
+ * Requires `validators`. `validateX` and `isX` are unchanged either way.
96
+ */
97
+ readonly coerce?: boolean;
98
+ /**
99
+ * When true, generated validators also get a `repairX`: it coerces, validates,
100
+ * and then repairs each position the validator rejected to a value the schema
101
+ * itself supplies — a `default`, a `const`, the first `enum` member, or a
102
+ * fallback built to satisfy the position's own bounds. The result carries the
103
+ * repaired value together with `repairs`, which *are* the errors `validateX`
104
+ * produced for the positions that were repaired, so a caller logs the same
105
+ * path, keyword and params it would have been rejected with.
106
+ *
107
+ * A document needing no repair comes back valid with an empty `repairs`; one
108
+ * fully repaired comes back valid with a non-empty one, leaving the caller to
109
+ * decide whether that is acceptable; one that could not be fully repaired
110
+ * comes back invalid carrying both the repairs applied and the errors still
111
+ * outstanding.
112
+ *
113
+ * Implies `coerce` — repairing runs after coercion, so a value merely written
114
+ * in the wrong type is right before the validator sees it and never counts as
115
+ * a repair. Requires `validators`.
116
+ */
117
+ readonly repair?: boolean;
118
+ /**
119
+ * When true, a failing `anyOf` / `oneOf` in a generated validator also reports
120
+ * the errors of the branch it meant, instead of the bare "must match a schema
121
+ * in anyOf" that names no field and no reason.
122
+ *
123
+ * Off by default, and off costs nothing — the generated code is exactly what
124
+ * it would be without the option. On, each branch keeps what it complained
125
+ * about, which is a push per *failing* branch and one lazily-created array per
126
+ * combinator that had one. A valid instance fails no branch and allocates
127
+ * nothing, but the branches are still handed a collector, so measure before
128
+ * turning it on in a hot path.
129
+ *
130
+ * Requires `validators`.
131
+ */
132
+ readonly branchErrors?: boolean;
54
133
  /**
55
134
  * When true, also emit test-data files for every schema: a `fast-check`
56
135
  * 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,26 @@ const run = async () => {
390
377
  const fileConfig = configPath ? await loadConfig(configPath) : {};
391
378
  const cliConfig = parseCliArgs(args);
392
379
  const config = { ...fileConfig, ...cliConfig };
380
+ if (config.coerce && !config.validators) {
381
+ console.error("Error: --coerce shapes the generated validators, so it needs --validators too.");
382
+ process.exit(1);
383
+ }
384
+ if (config.repair && !config.validators) {
385
+ console.error("Error: --repair shapes the generated validators, so it needs --validators too.");
386
+ process.exit(1);
387
+ }
388
+ if (config.check && !config.validators) {
389
+ console.error("Error: --check shapes the generated validators, so it needs --validators too.");
390
+ process.exit(1);
391
+ }
392
+ if (config.branchErrors && !config.validators) {
393
+ console.error("Error: --branch-errors shapes the generated validators, so it needs --validators too.");
394
+ process.exit(1);
395
+ }
393
396
  if (config.outDir && config.outFile) {
394
397
  console.error("Error: provide only one of --out-dir or --out-file, not both.");
395
398
  process.exit(1);
396
399
  }
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
400
  if (config.rootType !== void 0 && !ROOT_TYPE_PATTERN.test(config.rootType)) {
401
401
  console.error(`Error: invalid --root-type "${config.rootType}". Expected a TypeScript identifier (letters, digits, _ or $, not starting with a digit).`);
402
402
  process.exit(1);
@@ -1,4 +1,4 @@
1
- import type { GeneratedFile } from '@amritk/generate-parsers';
1
+ import type { GeneratedFile } from '@amritk/parsers';
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/parsers';
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/parsers` 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/parsers";
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 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 beside the parsers, over one\n shared declaration of each generated type\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
@@ -21,7 +21,17 @@ 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
+ --check Also emit checkX, the same result as validateX but stopped at the
27
+ first violation (requires --validators)
28
+ --coerce Also emit coerceX, which coerces scalars the way Ajv's coerceTypes
29
+ does and then validates (requires --validators)
30
+ --repair Also emit repairX, which coerces, validates, then repairs each
31
+ rejected position to a schema-supplied value and reports the
32
+ errors it repaired (implies --coerce; requires --validators)
33
+ --branch-errors Explain a failing anyOf/oneOf with the errors of the branch it
34
+ meant (requires --validators; costs a little on unions)
25
35
  --examples Also emit fast-check arbitraries + example values under examples/
26
36
  --message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)
27
37
  --discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)
@@ -34,6 +34,8 @@ const loadConfig = async (configPath) => {
34
34
  ...typeof obj["messageContracts"] === "boolean" && { messageContracts: obj["messageContracts"] },
35
35
  ...typeof obj["discriminator"] === "string" && { discriminator: obj["discriminator"] },
36
36
  ...typeof obj["build"] === "boolean" && { build: obj["build"] },
37
+ ...typeof obj["coerce"] === "boolean" && { coerce: obj["coerce"] },
38
+ ...typeof obj["branchErrors"] === "boolean" && { branchErrors: obj["branchErrors"] },
37
39
  ...typeof obj["force"] === "boolean" && { force: obj["force"] },
38
40
  ...typeof obj["logWarnings"] === "boolean" && { logWarnings: obj["logWarnings"] },
39
41
  ...typeof obj["strict"] === "boolean" && { strict: obj["strict"] },
@@ -4,6 +4,10 @@ const LIST_KEYS = /* @__PURE__ */ new Set(["allowedHosts", "allowedRoots"]);
4
4
  const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
5
5
  "typesOnly",
6
6
  "validators",
7
+ "check",
8
+ "coerce",
9
+ "repair",
10
+ "branchErrors",
7
11
  "examples",
8
12
  "messageContracts",
9
13
  "build",
@@ -127,6 +131,18 @@ const assignBoolean = (config, key, value) => {
127
131
  case "validators":
128
132
  config.validators = value;
129
133
  return true;
134
+ case "check":
135
+ config.check = value;
136
+ return true;
137
+ case "coerce":
138
+ config.coerce = value;
139
+ return true;
140
+ case "repair":
141
+ config.repair = value;
142
+ return true;
143
+ case "branchErrors":
144
+ config.branchErrors = value;
145
+ return true;
130
146
  case "examples":
131
147
  config.examples = value;
132
148
  return true;
@@ -0,0 +1,17 @@
1
+ import type { Mode } from '@amritk/parsers';
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
+ * Everything else always carries a parser, because emitting one is what the CLI
15
+ * does when you do not tell it otherwise.
16
+ */
17
+ export declare const resolveModes: (config: Partial<CliConfig>) => Mode[];
@@ -0,0 +1,19 @@
1
+ const resolveModes = (config) => {
2
+ if (config.typesOnly === true)
3
+ return ["types"];
4
+ const modes = ["types"];
5
+ if (config.validators === 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
+ modes.push(config.strict === true ? "parseStrict" : "parse");
15
+ return modes;
16
+ };
17
+ export {
18
+ resolveModes
19
+ };
@@ -9,6 +9,10 @@ const CONFIG_KEYS = {
9
9
  outFile: { type: "string" },
10
10
  typesOnly: { type: "boolean" },
11
11
  validators: { type: "boolean" },
12
+ check: { type: "boolean" },
13
+ coerce: { type: "boolean" },
14
+ repair: { type: "boolean" },
15
+ branchErrors: { type: "boolean" },
12
16
  examples: { type: "boolean" },
13
17
  messageContracts: { type: "boolean" },
14
18
  discriminator: { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/mjst",
3
- "version": "0.21.0",
3
+ "version": "0.22.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.2",
53
- "@amritk/api": "^0.16.4",
54
- "@amritk/asyncapi": "^0.3.0",
55
- "@amritk/generate-examples": "^0.8.4",
52
+ "@amritk/adapters": "^0.6.4",
53
+ "@amritk/api": "^0.16.5",
54
+ "@amritk/asyncapi": "^0.3.2",
55
+ "@amritk/generate-examples": "^0.8.6",
56
56
  "@amritk/generate-markdown": "^0.8.0",
57
- "@amritk/generate-parsers": "^0.23.0",
58
- "@amritk/generate-validators": "^0.17.0",
59
- "@amritk/helpers": "^0.21.0",
60
- "@amritk/lint": "^0.6.0",
57
+ "@amritk/helpers": "^0.23.0",
58
+ "@amritk/lint": "^0.6.1",
59
+ "@amritk/parsers": "^0.2.0",
61
60
  "@amritk/resolve-refs": "^0.7.1",
62
61
  "@amritk/yaml": "^0.7.2",
63
62
  "fast-glob": "^3.3.2",