@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 +1 -1
- package/README.md +62 -10
- package/dist/cli-config.d.ts +83 -4
- package/dist/cli.js +29 -29
- package/dist/combine-files.d.ts +1 -1
- package/dist/generate-files.d.ts +24 -0
- package/dist/generate-files.js +24 -0
- package/dist/help-text.d.ts +1 -1
- package/dist/help-text.js +11 -1
- package/dist/load-config.js +2 -0
- package/dist/parse-cli-args.js +16 -0
- package/dist/resolve-modes.d.ts +17 -0
- package/dist/resolve-modes.js +19 -0
- package/dist/validate-config.js +4 -0
- package/package.json +8 -9
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/
|
|
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
|
-
|
|
99
|
-
|
|
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,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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.
|
|
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.
|
|
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>
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -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.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|
package/dist/combine-files.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GeneratedFile } from '@amritk/
|
|
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
|
+
};
|
package/dist/help-text.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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)
|
package/dist/load-config.js
CHANGED
|
@@ -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"] },
|
package/dist/parse-cli-args.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/validate-config.js
CHANGED
|
@@ -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.
|
|
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.
|
|
53
|
-
"@amritk/api": "^0.16.
|
|
54
|
-
"@amritk/asyncapi": "^0.3.
|
|
55
|
-
"@amritk/generate-examples": "^0.8.
|
|
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/
|
|
58
|
-
"@amritk/
|
|
59
|
-
"@amritk/
|
|
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",
|