@amritk/mjst 0.19.0 → 0.21.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 +7 -2
- package/README.md +347 -1
- package/dist/cli-config.d.ts +9 -0
- package/dist/cli.js +10 -1
- package/dist/emit-message-contracts.d.ts +6 -0
- package/dist/emit-message-contracts.js +11 -2
- package/dist/help-text.d.ts +1 -1
- package/dist/help-text.js +4 -0
- package/dist/lint/preset-ruleset.d.ts +61 -0
- package/dist/lint/preset-ruleset.js +55 -0
- package/dist/lint/run.js +6 -14
- package/dist/load-config.d.ts +0 -8
- package/dist/load-config.js +2 -0
- package/dist/markdown/help-text.d.ts +6 -0
- package/dist/markdown/help-text.js +34 -0
- package/dist/markdown/parse-markdown-args.d.ts +34 -0
- package/dist/markdown/parse-markdown-args.js +97 -0
- package/dist/markdown/run.d.ts +9 -0
- package/dist/markdown/run.js +72 -0
- package/dist/parse-cli-args.js +6 -1
- package/dist/validate-config.d.ts +5 -2
- package/dist/validate-config.js +3 -0
- package/package.json +10 -10
package/AI.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @amritk/mjst (the `mjst` CLI) — notes for AI coding agents
|
|
2
2
|
|
|
3
3
|
The command-line entry point to mjst: generate TypeScript parsers, validators,
|
|
4
|
-
and types from JSON Schema, plus `lint
|
|
4
|
+
and types from JSON Schema, plus `lint`, `compile-api`, and `markdown`
|
|
5
|
+
subcommands. Full
|
|
5
6
|
reference is [README.md](./README.md); config options are in
|
|
6
7
|
[config.schema.json](./config.schema.json).
|
|
7
8
|
|
|
@@ -9,7 +10,7 @@ reference is [README.md](./README.md); config options are in
|
|
|
9
10
|
> package ships **only a `mjst` binary** — there are no JS exports. For the
|
|
10
11
|
> programmatic API import `@amritk/generate-parsers` etc. instead.
|
|
11
12
|
|
|
12
|
-
## The
|
|
13
|
+
## The four commands
|
|
13
14
|
|
|
14
15
|
```bash
|
|
15
16
|
# 1. Codegen (default command)
|
|
@@ -20,6 +21,10 @@ mjst lint "**/*.{yaml,json}" -r .lint.yaml
|
|
|
20
21
|
|
|
21
22
|
# 3. Compile an @amritk/api routes module to a fused, eval-free handler
|
|
22
23
|
mjst compile-api ./routes.ts --out ./dist/handler.ts
|
|
24
|
+
|
|
25
|
+
# 4. Render a schema as documentation (pages by default, --table for one table)
|
|
26
|
+
mjst markdown ./config.schema.json --out-dir ./docs
|
|
27
|
+
mjst markdown ./config.schema.json --table --readme ./README.md
|
|
23
28
|
```
|
|
24
29
|
|
|
25
30
|
## Codegen gotchas
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
Options can be supplied via CLI flags or a JSON config file. **CLI flags always take precedence over config file values.**
|
|
23
23
|
|
|
24
|
-
It also carries a `lint` subcommand — `mjst lint <files>` — a format-agnostic JSON/YAML style-guide linter powered by [`@amritk/lint`](../lint) (see [Linting](#linting)),
|
|
24
|
+
It also carries a `lint` subcommand — `mjst lint <files>` — a format-agnostic JSON/YAML style-guide linter powered by [`@amritk/lint`](../lint) (see [Linting](#linting)), a `compile-api` subcommand — `mjst compile-api <routes-module> --out <file>` — that compiles [`@amritk/api`](../api) route contracts into a production fetch-handler module (see [Compiling an API](#compiling-an-api)), and a `markdown` subcommand — `mjst markdown <schema>` — that renders a schema as documentation with [`@amritk/generate-markdown`](../generate-markdown) (see [Generating markdown docs](#generating-markdown-docs)).
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
@@ -217,6 +217,19 @@ config/service.yaml:2:7 error config-schema /port must be integer
|
|
|
217
217
|
|
|
218
218
|
With no `-r`, mjst discovers a `.lint.{yaml,yml,json,js,mjs}` ruleset by walking up from each linted file. A ruleset maps a JSONPath (`given`) to a function (`then`) — `schema` (JSON Schema), `casing`, `pattern`, `truthy`, and more, plus your own. The [`@amritk/lint` README](../lint#ruleset-format) documents the ruleset format and built-in functions. For machine-readable output (JSON, SARIF, …), call the [`@amritk/lint`](../lint) library's `lintDocument`, which returns structured findings.
|
|
219
219
|
|
|
220
|
+
Your own ruleset can build on a preset: `extends: [asyncapi]` (or `[oas]`) layers your rules on top of the preset's, with the preset's custom functions and format detection intact — through `-r <file>` and `.lint.*` discovery alike. Relative `extends` targets and custom `functions` still resolve next to your ruleset file. Extend at most one preset per ruleset: the two bring different formats and functions and neither can resolve the other, so a ruleset naming both is refused up front. Lint AsyncAPI and OpenAPI documents in separate runs.
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
# .lint.yaml — the AsyncAPI preset plus a house rule
|
|
224
|
+
extends:
|
|
225
|
+
- asyncapi
|
|
226
|
+
rules:
|
|
227
|
+
needs-terms-of-service:
|
|
228
|
+
given: "$.info"
|
|
229
|
+
severity: error
|
|
230
|
+
then: { field: termsOfService, function: truthy }
|
|
231
|
+
```
|
|
232
|
+
|
|
220
233
|
| Flag | Description |
|
|
221
234
|
| --- | --- |
|
|
222
235
|
| `<documents..>` | Files or globs to lint (positional). |
|
|
@@ -266,6 +279,330 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
266
279
|
|
|
267
280
|
---
|
|
268
281
|
|
|
282
|
+
## Generating markdown docs
|
|
283
|
+
|
|
284
|
+
`mjst markdown` renders a JSON Schema as documentation with
|
|
285
|
+
[`@amritk/generate-markdown`](../generate-markdown), so a config's docs cannot
|
|
286
|
+
drift from the schema that validates it. Like `lint` and `compile-api`, it has
|
|
287
|
+
its own flags (and its own `--help`), independent of the generation flags above.
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# The prose reference: a heading, a type, the description and an example per
|
|
291
|
+
# property, across as many pages as the schema's x-doc keyword declares
|
|
292
|
+
npx mjst markdown ./config.schema.json --out-dir ./docs
|
|
293
|
+
|
|
294
|
+
# The other shape: one HTML table, spliced into an existing markdown file
|
|
295
|
+
npx mjst markdown ./config.schema.json --table --readme ./README.md
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
| Flag | Description |
|
|
299
|
+
| --- | --- |
|
|
300
|
+
| `<schema>` | The JSON Schema to document (positional, required). |
|
|
301
|
+
| `--out-dir` | Directory the prose reference pages are written to (default: the current directory). |
|
|
302
|
+
| `--file` | Output path of the index page (default: the schema's `x-doc.file`, then `index.md`). |
|
|
303
|
+
| `--title` | Page title (default: the schema's `title`). |
|
|
304
|
+
| `--language` | Fence language for derived examples and literals (default: `json`). |
|
|
305
|
+
| `--layout` | Default layout for nested properties: `headings` (default), `table`, or `none`. |
|
|
306
|
+
| `--sort` | Property order: `schema` (default) or `alphabetical`. |
|
|
307
|
+
| `--heading-level` | Heading level of the page title, 1-6 (default: `1`). |
|
|
308
|
+
| `--table` | Render the HTML config table instead of the prose pages. |
|
|
309
|
+
| `--readme` | Markdown file the table is spliced into (default: `README.md`). Requires `--table`. |
|
|
310
|
+
|
|
311
|
+
Under `--table` only the content between `<!-- config-table-start -->
|
|
312
|
+
<table>
|
|
313
|
+
<thead>
|
|
314
|
+
<tr>
|
|
315
|
+
<th>Property</th>
|
|
316
|
+
<th>CLI Flag</th>
|
|
317
|
+
<th>Type</th>
|
|
318
|
+
<th align="center">Default</th>
|
|
319
|
+
</tr>
|
|
320
|
+
</thead>
|
|
321
|
+
<tbody>
|
|
322
|
+
<tr>
|
|
323
|
+
<td>📄 <code>schema</code></td>
|
|
324
|
+
<td><code>--schema <path></code></td>
|
|
325
|
+
<td><code>string</code></td>
|
|
326
|
+
<td align="center"></td>
|
|
327
|
+
</tr>
|
|
328
|
+
<tr>
|
|
329
|
+
<td colspan="4">Path to the schema to process. With the default 'json' input this is a JSON Schema file that is read and parsed. With any other input format it is a JS/TS module that exports a schema, which is loaded and converted to JSON Schema via the matching adapter. Either 'schema' or 'schemaDir' is required.<br><strong>Examples:</strong> <code>"./schema.json"</code></td>
|
|
330
|
+
</tr>
|
|
331
|
+
<tr>
|
|
332
|
+
<td>🗂️ <code>schemaDir</code></td>
|
|
333
|
+
<td><code>--schema-dir <dir></code></td>
|
|
334
|
+
<td><code>string</code></td>
|
|
335
|
+
<td align="center"></td>
|
|
336
|
+
</tr>
|
|
337
|
+
<tr>
|
|
338
|
+
<td colspan="4">Path to a directory of JSON Schema files. When set, the CLI walks the directory recursively, generates parsers for every '*.json' schema it finds, and mirrors the directory layout under outDir (each schema lands in its own subdirectory). The runtime helpers are emitted once into a shared outDir/_helpers/ that every nested parser imports from. Mutually exclusive with 'schema'; when both are present 'schemaDir' wins. Only JSON Schema input is supported in this mode.<br><strong>Examples:</strong> <code>"./schemas"</code></td>
|
|
339
|
+
</tr>
|
|
340
|
+
<tr>
|
|
341
|
+
<td>🔌 <code>input</code></td>
|
|
342
|
+
<td><code>--input <format></code></td>
|
|
343
|
+
<td><code>string</code></td>
|
|
344
|
+
<td align="center"><code>"json"</code></td>
|
|
345
|
+
</tr>
|
|
346
|
+
<tr>
|
|
347
|
+
<td colspan="4">Source format of the schema. 'json' (default) reads a JSON Schema file directly. 'avro' reads an Apache Avro '.avsc' JSON document and converts it, needing no extra library. 'asyncapi' reads an AsyncAPI 2.x/3.0 document (JSON or YAML) and generates from every message payload/headers schema it declares. Any other format loads 'schema' as a module and converts it to JSON Schema with the matching adapter. Supported: 'typebox', 'zod' (zod v4+), 'valibot' (with @valibot/to-json-schema), and 'effect' — each requires the corresponding library installed in your project.<br><strong>Allowed:</strong> <code>"json"</code>, <code>"typebox"</code>, <code>"zod"</code>, <code>"valibot"</code>, <code>"effect"</code>, <code>"avro"</code>, <code>"asyncapi"</code></td>
|
|
348
|
+
</tr>
|
|
349
|
+
<tr>
|
|
350
|
+
<td>📦 <code>export</code></td>
|
|
351
|
+
<td><code>--export <name></code></td>
|
|
352
|
+
<td><code>string</code></td>
|
|
353
|
+
<td align="center"></td>
|
|
354
|
+
</tr>
|
|
355
|
+
<tr>
|
|
356
|
+
<td colspan="4">Which export of the schema module to use when 'input' names a module format (anything but 'json', 'avro', and 'asyncapi', which are read as data). Defaults to the default export, or the sole named export when the module has exactly one.</td>
|
|
357
|
+
</tr>
|
|
358
|
+
<tr>
|
|
359
|
+
<td>📁 <code>outDir</code></td>
|
|
360
|
+
<td><code>--out-dir <dir></code></td>
|
|
361
|
+
<td><code>string</code></td>
|
|
362
|
+
<td align="center"></td>
|
|
363
|
+
</tr>
|
|
364
|
+
<tr>
|
|
365
|
+
<td colspan="4">Output directory for generated TypeScript files. The directory is created automatically if it does not exist. Subdirectories are created as needed when a generated file includes a nested path. Mutually exclusive with 'outFile'.<br><strong>Examples:</strong> <code>"./generated"</code></td>
|
|
366
|
+
</tr>
|
|
367
|
+
<tr>
|
|
368
|
+
<td>📄 <code>outFile</code></td>
|
|
369
|
+
<td><code>--out-file <file></code></td>
|
|
370
|
+
<td><code>string</code></td>
|
|
371
|
+
<td align="center"></td>
|
|
372
|
+
</tr>
|
|
373
|
+
<tr>
|
|
374
|
+
<td colspan="4">Output everything to a single TypeScript file instead of a directory. Every generated definition is concatenated into one self-contained file and the cross-file imports are dropped. Mutually exclusive with 'outDir'. Currently supported only together with 'typesOnly'.<br><strong>Examples:</strong> <code>"./generated/schema.ts"</code></td>
|
|
375
|
+
</tr>
|
|
376
|
+
<tr>
|
|
377
|
+
<td>🏷️ <code>typesOnly</code></td>
|
|
378
|
+
<td><code>--types-only</code></td>
|
|
379
|
+
<td><code>boolean</code></td>
|
|
380
|
+
<td align="center"><code>false</code></td>
|
|
381
|
+
</tr>
|
|
382
|
+
<tr>
|
|
383
|
+
<td colspan="4">Generate only TypeScript type definitions without parser functions. Useful when you only need the type shapes and do not need runtime validation.</td>
|
|
384
|
+
</tr>
|
|
385
|
+
<tr>
|
|
386
|
+
<td>✅ <code>validators</code></td>
|
|
387
|
+
<td><code>--validators</code></td>
|
|
388
|
+
<td><code>boolean</code></td>
|
|
389
|
+
<td align="center"><code>false</code></td>
|
|
390
|
+
</tr>
|
|
391
|
+
<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>
|
|
393
|
+
</tr>
|
|
394
|
+
<tr>
|
|
395
|
+
<td>🎲 <code>examples</code></td>
|
|
396
|
+
<td><code>--examples</code></td>
|
|
397
|
+
<td><code>boolean</code></td>
|
|
398
|
+
<td align="center"><code>false</code></td>
|
|
399
|
+
</tr>
|
|
400
|
+
<tr>
|
|
401
|
+
<td colspan="4">Also emit test-data files for every schema into an examples/ subdirectory of the output destination: a fast-check arbitrary (FooArbitrary) that produces schema-valid values and a concrete fooExample value. Under schemaDir the examples mirror the schema layout so they never collide with the parser output. The generated arbitraries import fast-check, which consumers must install as a (dev) dependency; the static example values have no runtime dependencies.</td>
|
|
402
|
+
</tr>
|
|
403
|
+
<tr>
|
|
404
|
+
<td>🔌 <code>messageContracts</code></td>
|
|
405
|
+
<td><code>--message-contracts</code></td>
|
|
406
|
+
<td><code>boolean</code></td>
|
|
407
|
+
<td align="center"><code>false</code></td>
|
|
408
|
+
</tr>
|
|
409
|
+
<tr>
|
|
410
|
+
<td colspan="4">Also emit a @amritk/api channel contract per AsyncAPI channel into a contracts/ subdirectory: one defineMessages({ ... }) module naming every message that channel carries — keyed by the value its discriminator property carries on the wire, with that property stripped out of each payload because the runtime removes it before validating — plus a barrel re-exporting them. Requires input 'asyncapi', which is the only input that has channels and operations to project from. Incompatible with typesOnly: a contract is a runtime value, not a type. The generated modules import @amritk/api, which the consuming project must install; it is not a dependency of the generated parsers.</td>
|
|
411
|
+
</tr>
|
|
412
|
+
<tr>
|
|
413
|
+
<td>🏷️ <code>discriminator</code></td>
|
|
414
|
+
<td><code>--discriminator <prop></code></td>
|
|
415
|
+
<td><code>string</code></td>
|
|
416
|
+
<td align="center"><code>"type"</code></td>
|
|
417
|
+
</tr>
|
|
418
|
+
<tr>
|
|
419
|
+
<td colspan="4">Property name that says which message a frame is, for the contracts messageContracts emits. Defaults to 'type', matching @amritk/api. A channel that declares its own with 'x-mjst': { 'discriminator': '...' } keeps it — one flag covers a whole run, and a run may span channels that disagree. Only meaningful together with messageContracts.<br><strong>Examples:</strong> <code>"event"</code></td>
|
|
420
|
+
</tr>
|
|
421
|
+
<tr>
|
|
422
|
+
<td>🔨 <code>build</code></td>
|
|
423
|
+
<td><code>--build</code></td>
|
|
424
|
+
<td><code>boolean</code></td>
|
|
425
|
+
<td align="center"><code>false</code></td>
|
|
426
|
+
</tr>
|
|
427
|
+
<tr>
|
|
428
|
+
<td colspan="4">Compile the generated TypeScript files to .js and .d.ts output. A temporary tsconfig is written to the output directory, tsc is invoked, and the intermediate .ts source files are removed when compilation succeeds.</td>
|
|
429
|
+
</tr>
|
|
430
|
+
<tr>
|
|
431
|
+
<td>💥 <code>force</code></td>
|
|
432
|
+
<td><code>--force</code></td>
|
|
433
|
+
<td><code>boolean</code></td>
|
|
434
|
+
<td align="center"><code>false</code></td>
|
|
435
|
+
</tr>
|
|
436
|
+
<tr>
|
|
437
|
+
<td colspan="4">Deprecated and ignored. Generated files always replace whatever is at their path, so this flag no longer does anything; it is still accepted so existing scripts and config files keep running. Reverting an unwanted replacement is version control's job. Compilation under build still only removes the intermediate sources the run itself generated.</td>
|
|
438
|
+
</tr>
|
|
439
|
+
<tr>
|
|
440
|
+
<td>⚠️ <code>logWarnings</code></td>
|
|
441
|
+
<td><code>--log-warnings</code></td>
|
|
442
|
+
<td><code>boolean</code></td>
|
|
443
|
+
<td align="center"><code>false</code></td>
|
|
444
|
+
</tr>
|
|
445
|
+
<tr>
|
|
446
|
+
<td colspan="4">Emit a console.warn in the generated parsers for every input key that is not declared in the schema's properties. Useful for detecting schema drift or unexpected data shapes at runtime.</td>
|
|
447
|
+
</tr>
|
|
448
|
+
<tr>
|
|
449
|
+
<td>🚫 <code>strict</code></td>
|
|
450
|
+
<td><code>--strict</code></td>
|
|
451
|
+
<td><code>boolean</code></td>
|
|
452
|
+
<td align="center"><code>false</code></td>
|
|
453
|
+
</tr>
|
|
454
|
+
<tr>
|
|
455
|
+
<td colspan="4">Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. When a schema sets additionalProperties: false, undeclared keys throw too; otherwise they are still allowed.</td>
|
|
456
|
+
</tr>
|
|
457
|
+
<tr>
|
|
458
|
+
<td>🧹 <code>stripUnknown</code></td>
|
|
459
|
+
<td><code>--strip-unknown</code></td>
|
|
460
|
+
<td><code>boolean</code></td>
|
|
461
|
+
<td align="center"><code>false</code></td>
|
|
462
|
+
</tr>
|
|
463
|
+
<tr>
|
|
464
|
+
<td colspan="4">Build each parser's result from the schema's declared properties only, silently dropping undeclared input keys at every nesting level (zod's .strip()). Extras are never a validation error, so this composes with strict (which still throws on wrong types and missing required properties) and yields to additionalProperties: false, which rejects rather than strips in strict mode.</td>
|
|
465
|
+
</tr>
|
|
466
|
+
<tr>
|
|
467
|
+
<td>🔡 <code>caseInsensitive</code></td>
|
|
468
|
+
<td><code>--case-insensitive</code></td>
|
|
469
|
+
<td><code>boolean</code></td>
|
|
470
|
+
<td align="center"><code>false</code></td>
|
|
471
|
+
</tr>
|
|
472
|
+
<tr>
|
|
473
|
+
<td colspan="4">Normalize a mis-cased string to the exact casing of a declared enum/const member it matches case-insensitively (e.g. hElLo → hello) instead of coercing to the default. Coerce mode only — strict parsers still reject a casing mismatch. Correctly-cased input keeps the exact-match fast path, so the hot path is unaffected.</td>
|
|
474
|
+
</tr>
|
|
475
|
+
<tr>
|
|
476
|
+
<td>🔒 <code>readonly</code></td>
|
|
477
|
+
<td><code>--readonly</code></td>
|
|
478
|
+
<td><code>boolean</code></td>
|
|
479
|
+
<td align="center"><code>false</code></td>
|
|
480
|
+
</tr>
|
|
481
|
+
<tr>
|
|
482
|
+
<td colspan="4">Emit every property, array, and record in the generated type definitions as readonly, producing deeply immutable types. Affects type definitions only; the generated parsers still build and return plain objects.</td>
|
|
483
|
+
</tr>
|
|
484
|
+
<tr>
|
|
485
|
+
<td>🔢 <code>unknownKeys</code></td>
|
|
486
|
+
<td><code>--unknown-keys <strategy></code></td>
|
|
487
|
+
<td><code>string</code></td>
|
|
488
|
+
<td align="center"><code>"count-keys"</code></td>
|
|
489
|
+
</tr>
|
|
490
|
+
<tr>
|
|
491
|
+
<td colspan="4">How generated fast paths (parsers and validators alike) prove a closed object — additionalProperties: false, or a stripUnknown build — carries no undeclared key. 'count-keys' compares Object.keys(obj).length and is the faster form on JavaScriptCore (Bun), where a for…in over a non-extensible object takes the engine's slow path and halves strict-parse throughput. 'count-enumerable' counts with for…in and allocates nothing; it was the faster form on Node 22, but on Node 26 'count-keys' ties or wins there too. Keep the default unless you are pinned to an older V8 and have measured your own shapes; the generated code never detects its runtime.<br><strong>Allowed:</strong> <code>"count-enumerable"</code>, <code>"count-keys"</code></td>
|
|
492
|
+
</tr>
|
|
493
|
+
<tr>
|
|
494
|
+
<td>🧪 <code>formats</code></td>
|
|
495
|
+
<td><code>--formats <list></code></td>
|
|
496
|
+
<td><code>string | array</code></td>
|
|
497
|
+
<td align="center"></td>
|
|
498
|
+
</tr>
|
|
499
|
+
<tr>
|
|
500
|
+
<td colspan="4">String formats the generated validators enforce: "all", or a comma-separated list (e.g. uuid,date-time,email). Omitted, format stays an annotation — JSON Schema's own reading, and what @amritk/runtime-validators does when given no formats. Set it to whatever validates the same schemas at runtime (@amritk/lint and createApi({ formats }) run the interpreter with formats on) so the build-time and runtime answers agree. Both validateX and the flat isX check them, against a formats.ts emitted alongside the validators.</td>
|
|
501
|
+
</tr>
|
|
502
|
+
<tr>
|
|
503
|
+
<td>🧰 <code>helpers</code></td>
|
|
504
|
+
<td><code>--helpers <mode></code></td>
|
|
505
|
+
<td><code>string</code></td>
|
|
506
|
+
<td align="center"></td>
|
|
507
|
+
</tr>
|
|
508
|
+
<tr>
|
|
509
|
+
<td colspan="4">Controls how generated parsers reference their runtime helpers. 'package' emits imports from @amritk/helpers (requires it to be installed in the consumer project). 'embedded' ships the helper source under outDir/_helpers/ so the output is self-contained. When omitted, the CLI auto-detects: it picks 'package' only when @amritk/helpers is a declared dependency (dependencies/devDependencies) of the nearest package.json above outDir, otherwise 'embedded'. Requiring a declaration (rather than mere resolvability) keeps auto-detected 'package' output portable across pnpm/isolated installs, where an undeclared transitive @amritk/helpers is not reachable at runtime.<br><strong>Allowed:</strong> <code>"package"</code>, <code>"embedded"</code></td>
|
|
510
|
+
</tr>
|
|
511
|
+
<tr>
|
|
512
|
+
<td>🏷️ <code>typeSuffix</code></td>
|
|
513
|
+
<td><code>--type-suffix <suffix></code></td>
|
|
514
|
+
<td><code>string</code></td>
|
|
515
|
+
<td align="center"><code>""</code></td>
|
|
516
|
+
</tr>
|
|
517
|
+
<tr>
|
|
518
|
+
<td colspan="4">Suffix appended to every generated type name derived from a $ref (e.g. 'Object' turns Contact into ContactObject). Defaults to no suffix. The root type name is used verbatim and is unaffected.</td>
|
|
519
|
+
</tr>
|
|
520
|
+
<tr>
|
|
521
|
+
<td>🪧 <code>banner</code></td>
|
|
522
|
+
<td><code>--banner [text]</code></td>
|
|
523
|
+
<td><code>boolean | string</code></td>
|
|
524
|
+
<td align="center"><code>false</code></td>
|
|
525
|
+
</tr>
|
|
526
|
+
<tr>
|
|
527
|
+
<td colspan="4">Prepend a comment header to every generated file (excluding _helpers/). true uses the default message ('This file was auto-generated by @amritk/mjst…'); a string uses that text as the message body, wrapped in a JSDoc block; false or omitted emits no header. On the CLI, --banner alone enables the default message and --banner <text> sets a custom one.</td>
|
|
528
|
+
</tr>
|
|
529
|
+
<tr>
|
|
530
|
+
<td>🔗 <code>importExt</code></td>
|
|
531
|
+
<td><code>--import-ext <ext></code></td>
|
|
532
|
+
<td><code>string</code></td>
|
|
533
|
+
<td align="center"><code>"ts"</code></td>
|
|
534
|
+
</tr>
|
|
535
|
+
<tr>
|
|
536
|
+
<td colspan="4">Extension emitted on every relative import specifier in the generated output (cross-file $ref imports, the index barrel, and embedded-helper imports). 'ts' (default) emits the literal on-disk paths so the generated .ts sources load directly under Bun, Node's type stripping (Node 22.6+ with --experimental-strip-types, unflagged from 22.18/23), and tsc when the consumer sets allowImportingTsExtensions — no build step required. 'js' is the standard TS NodeNext form ('./x.js' resolving to a sibling x.ts) for output you compile; it is selected automatically under 'build' since tsc refuses to emit from .ts specifiers. Passing --import-ext ts together with --build is an error.<br><strong>Allowed:</strong> <code>"js"</code>, <code>"ts"</code></td>
|
|
537
|
+
</tr>
|
|
538
|
+
<tr>
|
|
539
|
+
<td>🌱 <code>rootType</code></td>
|
|
540
|
+
<td><code>--root-type <name></code></td>
|
|
541
|
+
<td><code>string</code></td>
|
|
542
|
+
<td align="center"></td>
|
|
543
|
+
</tr>
|
|
544
|
+
<tr>
|
|
545
|
+
<td colspan="4">Name for the root type of a single --schema run (e.g. 'Program' yields parseProgram / validateProgram). When omitted, the name is derived from the schema's title, falling back to the schema filename in PascalCase (spec-plan.json → SpecPlan) and then to 'Document'. Not supported with --schema-dir, where each schema derives its own root type from its filename.<br><strong>Examples:</strong> <code>"Program"</code></td>
|
|
546
|
+
</tr>
|
|
547
|
+
<tr>
|
|
548
|
+
<td>🌐 <code>resolveRemote</code></td>
|
|
549
|
+
<td><code>--resolve-remote</code></td>
|
|
550
|
+
<td><code>boolean</code></td>
|
|
551
|
+
<td align="center"><code>false</code></td>
|
|
552
|
+
</tr>
|
|
553
|
+
<tr>
|
|
554
|
+
<td colspan="4">Allow fetching http(s) $refs while dereferencing a schema that contains cross-file or remote references. Off by default, so generation stays offline unless asked: a schema with a remote $ref fails rather than making a network call. Local cross-file $refs are always resolved and do not need this flag. A non-empty allowedHosts implies this.</td>
|
|
555
|
+
</tr>
|
|
556
|
+
<tr>
|
|
557
|
+
<td>🛡️ <code>allowedHosts</code></td>
|
|
558
|
+
<td><code>--allowed-hosts <hosts></code></td>
|
|
559
|
+
<td><code>array</code></td>
|
|
560
|
+
<td align="center"></td>
|
|
561
|
+
</tr>
|
|
562
|
+
<tr>
|
|
563
|
+
<td colspan="4">Restrict remote $ref fetches to these hosts (e.g. api.example.com). Accepts a comma-separated list or the flag repeated. Providing an allow-list is itself an opt-in to remote fetching (it implies resolveRemote), and each listed host bypasses the private-host guard.<br><strong>Examples:</strong> <code>["schemas.example.com"]</code></td>
|
|
564
|
+
</tr>
|
|
565
|
+
<tr>
|
|
566
|
+
<td>🔓 <code>allowPrivateHosts</code></td>
|
|
567
|
+
<td><code>--allow-private-hosts</code></td>
|
|
568
|
+
<td><code>boolean</code></td>
|
|
569
|
+
<td align="center"><code>false</code></td>
|
|
570
|
+
</tr>
|
|
571
|
+
<tr>
|
|
572
|
+
<td colspan="4">Permit remote $refs to private, loopback, and link-local hosts. Off by default: such targets are refused as a best-effort SSRF guard (notably the 169.254.169.254 cloud-metadata endpoint). An explicit allowedHosts entry always bypasses this guard.</td>
|
|
573
|
+
</tr>
|
|
574
|
+
<tr>
|
|
575
|
+
<td>📂 <code>allowedRoots</code></td>
|
|
576
|
+
<td><code>--allowed-roots <dirs></code></td>
|
|
577
|
+
<td><code>array</code></td>
|
|
578
|
+
<td align="center"></td>
|
|
579
|
+
</tr>
|
|
580
|
+
<tr>
|
|
581
|
+
<td colspan="4">Extra directories a local (cross-file) $ref may resolve into. By default a local $ref is confined to the schema's own directory, so '{"$ref": "../../../secrets.env"}' (or an absolute path) reads nothing. List a root here for the ordinary split-spec layout where v1/api.json refers to a shared common/user.json one level up. The schema's own directory is always allowed, so these roots widen the default rather than replacing it, and a $ref that still lands outside every root is refused. Accepts a comma-separated list or the flag repeated; relative entries resolve against the current working directory, from a config file as well as from the flag.<br><strong>Examples:</strong> <code>["./specs"]</code></td>
|
|
582
|
+
</tr>
|
|
583
|
+
<tr>
|
|
584
|
+
<td>⚙️ <code>config</code></td>
|
|
585
|
+
<td><code>--config <path></code></td>
|
|
586
|
+
<td><code>string</code></td>
|
|
587
|
+
<td align="center"></td>
|
|
588
|
+
</tr>
|
|
589
|
+
<tr>
|
|
590
|
+
<td colspan="4">Path to a JSON config file. Keys match the option names in this schema (schema, schemaDir, input, export, outDir, outFile, typesOnly, validators, examples, build, force, logWarnings, strict, stripUnknown, caseInsensitive, readonly, unknownKeys, helpers, typeSuffix, banner, importExt, rootType, resolveRemote, allowedHosts, allowPrivateHosts, allowedRoots) and are validated: an unknown key or a wrong value type fails the run instead of being ignored. CLI flags take precedence over config file values.<br><strong>Examples:</strong> <code>"./mjst.config.json"</code></td>
|
|
591
|
+
</tr>
|
|
592
|
+
</tbody>
|
|
593
|
+
</table>
|
|
594
|
+
<!-- config-table-end -->` is replaced; a file that exists without both markers
|
|
595
|
+
is left untouched and the run fails, rather than clobbering hand-written prose.
|
|
596
|
+
The prose pages have no such deal — the generator owns them and overwrites them
|
|
597
|
+
wholesale. The table and page flags are mutually exclusive: passing a page flag
|
|
598
|
+
with `--table` is a usage error rather than a silently ignored option.
|
|
599
|
+
|
|
600
|
+
The exit code is `0` on success, `1` when generation fails (malformed schema, a
|
|
601
|
+
readme without markers, a page declaration the schema contradicts), and `2` on a
|
|
602
|
+
usage error. Run `mjst markdown --help` for details.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
269
606
|
## Configuration reference
|
|
270
607
|
|
|
271
608
|
<!-- config-table-start -->
|
|
@@ -451,6 +788,15 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
451
788
|
<td colspan="4">How generated fast paths (parsers and validators alike) prove a closed object — additionalProperties: false, or a stripUnknown build — carries no undeclared key. 'count-keys' compares Object.keys(obj).length and is the faster form on JavaScriptCore (Bun), where a for…in over a non-extensible object takes the engine's slow path and halves strict-parse throughput. 'count-enumerable' counts with for…in and allocates nothing; it was the faster form on Node 22, but on Node 26 'count-keys' ties or wins there too. Keep the default unless you are pinned to an older V8 and have measured your own shapes; the generated code never detects its runtime.<br><strong>Allowed:</strong> <code>"count-enumerable"</code>, <code>"count-keys"</code></td>
|
|
452
789
|
</tr>
|
|
453
790
|
<tr>
|
|
791
|
+
<td>🧪 <code>formats</code></td>
|
|
792
|
+
<td><code>--formats <list></code></td>
|
|
793
|
+
<td><code>string | array</code></td>
|
|
794
|
+
<td align="center"></td>
|
|
795
|
+
</tr>
|
|
796
|
+
<tr>
|
|
797
|
+
<td colspan="4">String formats the generated validators enforce: "all", or a comma-separated list (e.g. uuid,date-time,email). Omitted, format stays an annotation — JSON Schema's own reading, and what @amritk/runtime-validators does when given no formats. Set it to whatever validates the same schemas at runtime (@amritk/lint and createApi({ formats }) run the interpreter with formats on) so the build-time and runtime answers agree. Both validateX and the flat isX check them, against a formats.ts emitted alongside the validators.</td>
|
|
798
|
+
</tr>
|
|
799
|
+
<tr>
|
|
454
800
|
<td>🧰 <code>helpers</code></td>
|
|
455
801
|
<td><code>--helpers <mode></code></td>
|
|
456
802
|
<td><code>string</code></td>
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -129,6 +129,15 @@ export type CliConfig = {
|
|
|
129
129
|
* (Node). Applies to parsers and validators alike.
|
|
130
130
|
*/
|
|
131
131
|
readonly unknownKeys?: UnknownKeysStrategy;
|
|
132
|
+
/**
|
|
133
|
+
* String `format`s the generated validators enforce — `'all'`, or the names to
|
|
134
|
+
* check. Left unset, `format` stays an annotation, which is 2020-12's own
|
|
135
|
+
* reading and what `@amritk/runtime-validators` does when given no formats.
|
|
136
|
+
*
|
|
137
|
+
* Set it to whatever the code validating the same schemas at runtime uses, so
|
|
138
|
+
* the build-time and runtime answers agree.
|
|
139
|
+
*/
|
|
140
|
+
readonly formats?: 'all' | readonly string[];
|
|
132
141
|
/**
|
|
133
142
|
* Controls how generated parsers reference their runtime helpers.
|
|
134
143
|
* - `'package'`: emit `import ... from '@amritk/helpers/...'`.
|
package/dist/cli.js
CHANGED
|
@@ -139,7 +139,7 @@ const findJsonSchemas = async (dir) => {
|
|
|
139
139
|
return results;
|
|
140
140
|
};
|
|
141
141
|
const runValidators = async (config, schema, rootTypeName, writer, validatorsSubDir) => {
|
|
142
|
-
const files = await buildValidatorSchema(schema, rootTypeName, config.typeSuffix, void 0, config.unknownKeys);
|
|
142
|
+
const files = await buildValidatorSchema(schema, rootTypeName, config.typeSuffix, void 0, config.unknownKeys, config.formats);
|
|
143
143
|
const staged = [];
|
|
144
144
|
for (const file of files) {
|
|
145
145
|
const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
|
|
@@ -368,6 +368,15 @@ const run = async () => {
|
|
|
368
368
|
process.stderr.write(result.stderr);
|
|
369
369
|
process.exit(result.code);
|
|
370
370
|
}
|
|
371
|
+
if (args[0] === "markdown") {
|
|
372
|
+
const { run: markdownRun } = await import("./markdown/run.js");
|
|
373
|
+
const result = await markdownRun(args.slice(1));
|
|
374
|
+
if (result.stdout)
|
|
375
|
+
process.stdout.write(result.stdout);
|
|
376
|
+
if (result.stderr)
|
|
377
|
+
process.stderr.write(result.stderr);
|
|
378
|
+
process.exit(result.code);
|
|
379
|
+
}
|
|
371
380
|
const metaRequest = parseMetaRequest(args);
|
|
372
381
|
if (metaRequest === "version") {
|
|
373
382
|
console.log(await readVersion());
|
|
@@ -38,6 +38,12 @@ export type EmitMessageContractsResult = {
|
|
|
38
38
|
* accepts no frame in either direction, which is a trap rather than a document.
|
|
39
39
|
* The reasons come back in {@link EmitMessageContractsResult.issues}.
|
|
40
40
|
*
|
|
41
|
+
* Only payloads are projected. A message's `headers` schema is generated as its
|
|
42
|
+
* own tree by the parser run, but has no slot in a messages contract — the
|
|
43
|
+
* contract describes WebSocket frames, and headers are the broker's layer. Each
|
|
44
|
+
* module says so in its header when the channel declares any, rather than
|
|
45
|
+
* leaving the omission to be discovered.
|
|
46
|
+
*
|
|
41
47
|
* The barrel's specifiers are always `.ts`, ignoring `--import-ext`: these
|
|
42
48
|
* modules are the one part of the output `--build` leaves uncompiled (they
|
|
43
49
|
* import the `@amritk/api` peer, and handing tsc an import the output's project
|
|
@@ -11,12 +11,19 @@ ${indent} `);
|
|
|
11
11
|
${entries.join("\n")}
|
|
12
12
|
${indent}},`;
|
|
13
13
|
};
|
|
14
|
-
const renderChannelModule = (contract, channel) => {
|
|
14
|
+
const renderChannelModule = (contract, channel, headerMessages) => {
|
|
15
15
|
const directions = [
|
|
16
16
|
Object.keys(contract.clientToServer).length > 0 ? renderDirection("clientToServer", contract.clientToServer, " ") : void 0,
|
|
17
17
|
Object.keys(contract.serverToClient).length > 0 ? renderDirection("serverToClient", contract.serverToClient, " ") : void 0
|
|
18
18
|
].filter((entry) => entry !== void 0);
|
|
19
19
|
const address = channel.address !== void 0 && channel.address !== channel.key ? ` (${channel.address})` : "";
|
|
20
|
+
const headerNote = headerMessages.length === 0 ? [] : [
|
|
21
|
+
" *",
|
|
22
|
+
" * Payloads only: a message contract describes WebSocket frames, which carry",
|
|
23
|
+
" * no headers of their own. These messages declare a headers schema, and its",
|
|
24
|
+
` * types were generated alongside the payload's, under \`<message>-headers/\`:`,
|
|
25
|
+
` * ${headerMessages.join(", ")}.`
|
|
26
|
+
];
|
|
20
27
|
return [
|
|
21
28
|
"/**",
|
|
22
29
|
` * Message contract for the AsyncAPI channel \`${channel.key}\`${address}.`,
|
|
@@ -29,6 +36,7 @@ const renderChannelModule = (contract, channel) => {
|
|
|
29
36
|
` * \`{ "${contract.discriminator}": "<key>", ... }\` selects its schema by that key, and the`,
|
|
30
37
|
" * tag is removed before the payload below is validated \u2014 which is why no",
|
|
31
38
|
" * payload here declares it.",
|
|
39
|
+
...headerNote,
|
|
32
40
|
" */",
|
|
33
41
|
`import { defineMessages } from '${CONTRACTS_PEER}'`,
|
|
34
42
|
"",
|
|
@@ -70,7 +78,8 @@ const emitMessageContracts = async (options) => {
|
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
80
|
const filename = `${CONTRACTS_DIR}/${token}.ts`;
|
|
73
|
-
|
|
81
|
+
const headerMessages = channel.messages.filter((message) => message.headers !== void 0).map(({ name }) => name);
|
|
82
|
+
await writer.stage(filename, bannerPrefix + renderChannelModule({ ...contract, exportName }, channel, headerMessages));
|
|
74
83
|
files.push(filename);
|
|
75
84
|
barrel.push(`export { ${exportName} } from './${token}.ts'`);
|
|
76
85
|
}
|
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\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\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 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";
|
package/dist/help-text.js
CHANGED
|
@@ -8,6 +8,7 @@ Usage:
|
|
|
8
8
|
Subcommands (each with its own flags and --help):
|
|
9
9
|
mjst lint <files> Lint JSON/YAML documents against a ruleset
|
|
10
10
|
mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module
|
|
11
|
+
mjst markdown <schema> --out-dir <dir> Generate markdown documentation from a JSON Schema
|
|
11
12
|
|
|
12
13
|
Input:
|
|
13
14
|
--schema <path> Schema to process: a JSON Schema file, or a module when --input is set
|
|
@@ -40,6 +41,9 @@ Validation:
|
|
|
40
41
|
--log-warnings console.warn on input keys not declared in the schema
|
|
41
42
|
--unknown-keys <s> How closed objects are proven free of undeclared keys:
|
|
42
43
|
count-keys (default, fastest on Bun) or count-enumerable (fastest on Node)
|
|
44
|
+
--formats <f> String formats the generated validators enforce: "all", or a
|
|
45
|
+
comma-separated list (e.g. uuid,date-time,email). Off by default,
|
|
46
|
+
which leaves "format" an annotation as JSON Schema reads it.
|
|
43
47
|
|
|
44
48
|
Reference resolution:
|
|
45
49
|
--resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Ruleset } from '@amritk/lint';
|
|
2
|
+
import type { RulesetDefinition } from '@amritk/lint/types';
|
|
3
|
+
/**
|
|
4
|
+
* Routing a ruleset to the right builder: which names mean a built-in preset,
|
|
5
|
+
* whether a ruleset the CLI loaded off disk names one, and how to build with it.
|
|
6
|
+
* The three live together rather than one per file because they are a single
|
|
7
|
+
* concern — the name table is the whole question — and because the two entry
|
|
8
|
+
* points must agree on it: `--ruleset <name>` and a loaded ruleset's `extends`
|
|
9
|
+
* accept exactly the same preset names.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The two built-in presets the CLI can build a ruleset with. They are mutually
|
|
13
|
+
* exclusive: each preset only knows how to resolve its *own* name inside
|
|
14
|
+
* `extends`, and each brings a different format registry, so one ruleset cannot
|
|
15
|
+
* be built with both.
|
|
16
|
+
*/
|
|
17
|
+
export type PresetName = 'asyncapi' | 'oas';
|
|
18
|
+
/**
|
|
19
|
+
* Every name that resolves to a built-in preset — as `--ruleset <name>` and
|
|
20
|
+
* inside a ruleset's `extends` — mapped to the preset it names. The aliases
|
|
21
|
+
* mirror what the presets' own `extends` resolution accepts, including the
|
|
22
|
+
* legacy Spectral ones.
|
|
23
|
+
*
|
|
24
|
+
* A Map, not a record: the key comes straight from user input, and a record
|
|
25
|
+
* lookup on `constructor` would find `Object.prototype`'s.
|
|
26
|
+
*/
|
|
27
|
+
export declare const PRESET_RULESET_NAMES: ReadonlyMap<string, PresetName>;
|
|
28
|
+
/**
|
|
29
|
+
* Builds a *runnable* ruleset with a preset's builder. Built rather than handed
|
|
30
|
+
* to `lintDocument` as data, deliberately: a preset brings its own custom
|
|
31
|
+
* functions and format detectors, which a definition cannot carry — as data,
|
|
32
|
+
* every one of its rules would be silently skipped (unknown functions, a
|
|
33
|
+
* `formats` gate matching nothing).
|
|
34
|
+
*
|
|
35
|
+
* With no `definition` this is the bare `--ruleset asyncapi` case: the preset's
|
|
36
|
+
* recommended rules and nothing else. With one, the caller's ruleset is built by
|
|
37
|
+
* the preset, whose `extends` resolution knows the preset names and whose
|
|
38
|
+
* `basePath` keeps the user's relative `extends` and `functions` resolving next
|
|
39
|
+
* to their own file.
|
|
40
|
+
*
|
|
41
|
+
* No `restrictTo` is passed, which matches what the core path does for a
|
|
42
|
+
* discovered or `--ruleset` file today: the CLI treats a ruleset on the local
|
|
43
|
+
* disk as trusted configuration. (`--allowed-roots` fences `$ref` resolution in
|
|
44
|
+
* the *document*, a separate concern.) See the "Trust boundary" section of
|
|
45
|
+
* `@amritk/lint`'s README.
|
|
46
|
+
*/
|
|
47
|
+
export declare const buildPresetRuleset: (preset: PresetName, definition?: RulesetDefinition, basePath?: string) => Promise<Ruleset>;
|
|
48
|
+
/**
|
|
49
|
+
* Decides what `lintDocument` should run for a ruleset just loaded off disk —
|
|
50
|
+
* whether it came from `--ruleset <file>` or `.lint.*` discovery.
|
|
51
|
+
*
|
|
52
|
+
* A ruleset that extends a built-in preset has to be built *here*, by that
|
|
53
|
+
* preset's builder: handed over as a definition, `lintDocument` builds it with
|
|
54
|
+
* the core `createRuleset`, which knows neither the preset names nor the
|
|
55
|
+
* functions and format detectors a preset brings. Anything else stays a
|
|
56
|
+
* definition and takes the core path exactly as it did before.
|
|
57
|
+
*
|
|
58
|
+
* @param basePath - The ruleset file's own directory, so its relative `extends` and `functions` still resolve there.
|
|
59
|
+
* @param label - How to name the ruleset in an error, e.g. its path.
|
|
60
|
+
*/
|
|
61
|
+
export declare const buildLoadedRuleset: (definition: RulesetDefinition, basePath: string, label: string) => Promise<RulesetDefinition | Ruleset>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const PRESET_RULESET_NAMES = /* @__PURE__ */ new Map([
|
|
2
|
+
["asyncapi", "asyncapi"],
|
|
3
|
+
["loupe:asyncapi", "asyncapi"],
|
|
4
|
+
["spectral:asyncapi", "asyncapi"],
|
|
5
|
+
["oas", "oas"],
|
|
6
|
+
["loupe:oas", "oas"],
|
|
7
|
+
["spectral:oas", "oas"]
|
|
8
|
+
]);
|
|
9
|
+
const collectPresets = (definition, into, seen) => {
|
|
10
|
+
if (typeof definition !== "object" || definition === null)
|
|
11
|
+
return;
|
|
12
|
+
if (seen.has(definition))
|
|
13
|
+
return;
|
|
14
|
+
seen.add(definition);
|
|
15
|
+
if (!definition.extends)
|
|
16
|
+
return;
|
|
17
|
+
const entries = Array.isArray(definition.extends) ? definition.extends : [definition.extends];
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const target = Array.isArray(entry) ? entry[0] : entry;
|
|
20
|
+
if (typeof target === "string") {
|
|
21
|
+
const preset = PRESET_RULESET_NAMES.get(target);
|
|
22
|
+
if (preset && !into.has(preset))
|
|
23
|
+
into.set(preset, target);
|
|
24
|
+
} else {
|
|
25
|
+
collectPresets(target, into, seen);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const findExtendedPreset = (definition, label) => {
|
|
30
|
+
const found = /* @__PURE__ */ new Map();
|
|
31
|
+
collectPresets(definition, found, /* @__PURE__ */ new Set());
|
|
32
|
+
const asyncapi = found.get("asyncapi");
|
|
33
|
+
const oas = found.get("oas");
|
|
34
|
+
if (asyncapi && oas) {
|
|
35
|
+
throw new Error(`Ruleset ${label} extends both built-in presets ("${asyncapi}" and "${oas}"), which cannot be combined: each preset brings its own functions and format detectors, and neither can resolve the other. Extend at most one preset per ruleset, and lint AsyncAPI and OpenAPI documents in separate runs.`);
|
|
36
|
+
}
|
|
37
|
+
return asyncapi ? "asyncapi" : oas ? "oas" : void 0;
|
|
38
|
+
};
|
|
39
|
+
const buildPresetRuleset = async (preset, definition, basePath) => {
|
|
40
|
+
if (preset === "asyncapi") {
|
|
41
|
+
const { createAsyncApiRuleset } = await import("@amritk/lint/rules/asyncapi");
|
|
42
|
+
return createAsyncApiRuleset(definition, basePath);
|
|
43
|
+
}
|
|
44
|
+
const { createOpenApiRuleset } = await import("@amritk/lint/rules/openapi");
|
|
45
|
+
return createOpenApiRuleset(definition, basePath);
|
|
46
|
+
};
|
|
47
|
+
const buildLoadedRuleset = async (definition, basePath, label) => {
|
|
48
|
+
const preset = findExtendedPreset(definition, label);
|
|
49
|
+
return preset ? await buildPresetRuleset(preset, definition, basePath) : definition;
|
|
50
|
+
};
|
|
51
|
+
export {
|
|
52
|
+
PRESET_RULESET_NAMES,
|
|
53
|
+
buildLoadedRuleset,
|
|
54
|
+
buildPresetRuleset
|
|
55
|
+
};
|
package/dist/lint/run.js
CHANGED
|
@@ -4,6 +4,7 @@ import { lintDocument, validateRuleset } from "@amritk/lint";
|
|
|
4
4
|
import { DiagnosticSeverity } from "@amritk/lint/types";
|
|
5
5
|
import fg from "fast-glob";
|
|
6
6
|
import yargs from "yargs";
|
|
7
|
+
import { buildLoadedRuleset, buildPresetRuleset, PRESET_RULESET_NAMES } from "./preset-ruleset.js";
|
|
7
8
|
import { createLintResolver } from "./resolver.js";
|
|
8
9
|
import { discoverRuleset, loadRuleset } from "./ruleset-loader.js";
|
|
9
10
|
const SEVERITY_BY_NAME = {
|
|
@@ -13,16 +14,6 @@ const SEVERITY_BY_NAME = {
|
|
|
13
14
|
hint: DiagnosticSeverity.Hint
|
|
14
15
|
};
|
|
15
16
|
const SEVERITY_LABEL = ["error", "warning", "info", "hint"];
|
|
16
|
-
const buildAsyncApiPreset = async () => (await import("@amritk/lint/rules/asyncapi")).createAsyncApiRuleset();
|
|
17
|
-
const buildOpenApiPreset = async () => (await import("@amritk/lint/rules/openapi")).createOpenApiRuleset();
|
|
18
|
-
const PRESET_RULESETS = /* @__PURE__ */ new Map([
|
|
19
|
-
["asyncapi", buildAsyncApiPreset],
|
|
20
|
-
["loupe:asyncapi", buildAsyncApiPreset],
|
|
21
|
-
["spectral:asyncapi", buildAsyncApiPreset],
|
|
22
|
-
["oas", buildOpenApiPreset],
|
|
23
|
-
["loupe:oas", buildOpenApiPreset],
|
|
24
|
-
["spectral:oas", buildOpenApiPreset]
|
|
25
|
-
]);
|
|
26
17
|
const formatReport = (findings) => {
|
|
27
18
|
if (findings.length === 0)
|
|
28
19
|
return "No problems found\n";
|
|
@@ -136,19 +127,20 @@ Run \`mjst lint --help\` for the supported flags.
|
|
|
136
127
|
return void 0;
|
|
137
128
|
const definition = await loadRuleset(discovered);
|
|
138
129
|
reportRulesetProblems(definition, discovered);
|
|
139
|
-
|
|
130
|
+
const basePath = dirname(discovered);
|
|
131
|
+
return { definition: await buildLoadedRuleset(definition, basePath, discovered), basePath };
|
|
140
132
|
};
|
|
141
133
|
let rulesetDefinition;
|
|
142
134
|
let rulesetBasePath;
|
|
143
135
|
if (parsed.ruleset) {
|
|
144
|
-
const preset =
|
|
136
|
+
const preset = PRESET_RULESET_NAMES.get(parsed.ruleset);
|
|
145
137
|
if (preset) {
|
|
146
|
-
rulesetDefinition = await preset
|
|
138
|
+
rulesetDefinition = await buildPresetRuleset(preset);
|
|
147
139
|
} else {
|
|
148
140
|
const definition = await loadRuleset(parsed.ruleset);
|
|
149
|
-
rulesetDefinition = definition;
|
|
150
141
|
rulesetBasePath = dirname(isAbsolute(parsed.ruleset) ? parsed.ruleset : resolve(process.cwd(), parsed.ruleset));
|
|
151
142
|
reportRulesetProblems(definition, parsed.ruleset);
|
|
143
|
+
rulesetDefinition = await buildLoadedRuleset(definition, rulesetBasePath, parsed.ruleset);
|
|
152
144
|
}
|
|
153
145
|
}
|
|
154
146
|
const documents = parsed.documents ?? [];
|
package/dist/load-config.d.ts
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
1
|
import type { CliConfig } from './cli-config.js';
|
|
2
|
-
/**
|
|
3
|
-
* Loads a JSON config file and returns the relevant CLI config properties.
|
|
4
|
-
* The config file should have the same keys as the CLI flags (schema, outDir).
|
|
5
|
-
*
|
|
6
|
-
* Every key is validated up front (see {@link validateConfig}), so the `typeof`
|
|
7
|
-
* guards below are only here to narrow the types for TypeScript — by the time we
|
|
8
|
-
* reach them, nothing can fail them.
|
|
9
|
-
*/
|
|
10
2
|
export declare const loadConfig: (configPath: string) => Promise<Partial<CliConfig>>;
|
package/dist/load-config.js
CHANGED
|
@@ -7,6 +7,7 @@ const isSourceFormat = (value) => typeof value === "string" && SOURCE_FORMATS.in
|
|
|
7
7
|
const isHelpersMode = (value) => value === "package" || value === "embedded";
|
|
8
8
|
const isImportExt = (value) => value === "js" || value === "ts";
|
|
9
9
|
const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
10
|
+
const isFormatsOption = (value) => value === "all" || Array.isArray(value) && value.every((name) => typeof name === "string");
|
|
10
11
|
const loadConfig = async (configPath) => {
|
|
11
12
|
const absolutePath = resolve(configPath);
|
|
12
13
|
const raw = await readFile(absolutePath, "utf-8").catch((error) => {
|
|
@@ -40,6 +41,7 @@ const loadConfig = async (configPath) => {
|
|
|
40
41
|
...typeof obj["caseInsensitive"] === "boolean" && { caseInsensitive: obj["caseInsensitive"] },
|
|
41
42
|
...typeof obj["readonly"] === "boolean" && { readonly: obj["readonly"] },
|
|
42
43
|
...isUnknownKeysStrategy(obj["unknownKeys"]) && { unknownKeys: obj["unknownKeys"] },
|
|
44
|
+
...isFormatsOption(obj["formats"]) && { formats: obj["formats"] },
|
|
43
45
|
...isHelpersMode(obj["helpers"]) && { helpers: obj["helpers"] },
|
|
44
46
|
...isImportExt(obj["importExt"]) && { importExt: obj["importExt"] },
|
|
45
47
|
...typeof obj["rootType"] === "string" && { rootType: obj["rootType"] },
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage text for `mjst markdown --help`. Hand-written like the top-level
|
|
3
|
+
* HELP_TEXT: the flag list is small and stable, and the test cross-checks that
|
|
4
|
+
* every flag is listed.
|
|
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";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const MARKDOWN_HELP_TEXT = `mjst markdown \u2014 generate markdown documentation from a JSON Schema
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
mjst markdown <schema> --out-dir <dir> [options]
|
|
5
|
+
mjst markdown <schema> --table --readme <file>
|
|
6
|
+
|
|
7
|
+
Two shapes come out of the same schema. By default it writes the prose
|
|
8
|
+
reference \u2014 a heading, a type, the description and an example per property,
|
|
9
|
+
across as many pages as the schema's x-doc keyword declares. With --table it
|
|
10
|
+
renders one HTML table instead and splices it into an existing markdown file
|
|
11
|
+
between <!-- config-table-start --> and <!-- config-table-end -->, leaving the
|
|
12
|
+
rest of that file alone.
|
|
13
|
+
|
|
14
|
+
Prose reference options:
|
|
15
|
+
--out-dir <dir> Directory the pages are written to (default: the current directory)
|
|
16
|
+
--file <path> Output path of the index page (default: the schema's x-doc.file, then index.md)
|
|
17
|
+
--title <text> Page title (default: the schema's title)
|
|
18
|
+
--language <lang> Fence language for derived examples and literals (default: json)
|
|
19
|
+
--layout <mode> Default layout for nested properties: headings (default), table, or none
|
|
20
|
+
--sort <order> Property order: schema (default) or alphabetical
|
|
21
|
+
--heading-level <n> Heading level of the page title, 1-6 (default: 1)
|
|
22
|
+
|
|
23
|
+
Table options:
|
|
24
|
+
--table Render the HTML config table instead of the prose pages
|
|
25
|
+
--readme <path> Markdown file the table is spliced into (default: README.md)
|
|
26
|
+
|
|
27
|
+
Misc:
|
|
28
|
+
--help, -h Print this help
|
|
29
|
+
|
|
30
|
+
Docs: https://github.com/amritk/mjst/tree/main/packages/cli#readme
|
|
31
|
+
`;
|
|
32
|
+
export {
|
|
33
|
+
MARKDOWN_HELP_TEXT
|
|
34
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DocLayout, DocSort } from '@amritk/generate-markdown';
|
|
2
|
+
/** The flags and positional the `markdown` subcommand understands. */
|
|
3
|
+
export type MarkdownArgs = {
|
|
4
|
+
/** Path to the JSON Schema to document (the sole positional). */
|
|
5
|
+
schema?: string;
|
|
6
|
+
/** Directory the prose reference pages are written to (`--out-dir`). */
|
|
7
|
+
outDir?: string;
|
|
8
|
+
/** Output path of the index page (`--file`). */
|
|
9
|
+
file?: string;
|
|
10
|
+
/** Page title override (`--title`). */
|
|
11
|
+
title?: string;
|
|
12
|
+
/** Fence language for derived examples (`--language`). */
|
|
13
|
+
language?: string;
|
|
14
|
+
/** Default layout for nested properties (`--layout`). */
|
|
15
|
+
layout?: DocLayout;
|
|
16
|
+
/** Property order (`--sort`). */
|
|
17
|
+
sort?: DocSort;
|
|
18
|
+
/** Heading level of the page title (`--heading-level`). */
|
|
19
|
+
headingLevel?: number;
|
|
20
|
+
/** True when `--table` was passed: render the HTML table instead of the pages. */
|
|
21
|
+
table?: boolean;
|
|
22
|
+
/** Markdown file the table is spliced into (`--readme`). */
|
|
23
|
+
readme?: string;
|
|
24
|
+
/** True when `--help`/`-h` was passed. */
|
|
25
|
+
help?: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parses the `markdown` argv into a typed args object. Mirrors the other
|
|
29
|
+
* subcommand parsers: kebab-case and camelCase flag spellings, both
|
|
30
|
+
* `--flag value` and `--flag=value`, and a loud error on unknown flags so a
|
|
31
|
+
* typo cannot silently drop an option. Throws on usage errors; the caller turns
|
|
32
|
+
* that into the exit-code-2 path.
|
|
33
|
+
*/
|
|
34
|
+
export declare const parseMarkdownArgs: (argv: readonly string[]) => MarkdownArgs;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const VALUE_KEYS = /* @__PURE__ */ new Set(["outDir", "file", "title", "language", "layout", "sort", "headingLevel", "readme"]);
|
|
2
|
+
const BOOLEAN_KEYS = /* @__PURE__ */ new Set(["table"]);
|
|
3
|
+
const LAYOUTS = ["headings", "table", "none"];
|
|
4
|
+
const SORTS = ["schema", "alphabetical"];
|
|
5
|
+
const toCamelCase = (key) => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
6
|
+
const parseChoice = (flag, value, choices) => {
|
|
7
|
+
const match = choices.find((choice) => choice === value);
|
|
8
|
+
if (match === void 0) {
|
|
9
|
+
throw new Error(`Invalid --${flag} value "${value}". Expected one of: ${choices.join(", ")}.`);
|
|
10
|
+
}
|
|
11
|
+
return match;
|
|
12
|
+
};
|
|
13
|
+
const parseHeadingLevel = (value) => {
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 6) {
|
|
16
|
+
throw new Error(`Invalid --heading-level value "${value}". Expected an integer from 1 to 6.`);
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
};
|
|
20
|
+
const assignValue = (args, key, value) => {
|
|
21
|
+
switch (key) {
|
|
22
|
+
case "outDir":
|
|
23
|
+
args.outDir = value;
|
|
24
|
+
return;
|
|
25
|
+
case "file":
|
|
26
|
+
args.file = value;
|
|
27
|
+
return;
|
|
28
|
+
case "title":
|
|
29
|
+
args.title = value;
|
|
30
|
+
return;
|
|
31
|
+
case "language":
|
|
32
|
+
args.language = value;
|
|
33
|
+
return;
|
|
34
|
+
case "layout":
|
|
35
|
+
args.layout = parseChoice("layout", value, LAYOUTS);
|
|
36
|
+
return;
|
|
37
|
+
case "sort":
|
|
38
|
+
args.sort = parseChoice("sort", value, SORTS);
|
|
39
|
+
return;
|
|
40
|
+
case "headingLevel":
|
|
41
|
+
args.headingLevel = parseHeadingLevel(value);
|
|
42
|
+
return;
|
|
43
|
+
case "readme":
|
|
44
|
+
args.readme = value;
|
|
45
|
+
return;
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unknown flag "--${key}".`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const parseMarkdownArgs = (argv) => {
|
|
51
|
+
const args = {};
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
const arg = argv[i];
|
|
54
|
+
if (arg === void 0)
|
|
55
|
+
continue;
|
|
56
|
+
if (arg === "--help" || arg === "-h") {
|
|
57
|
+
args.help = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!arg.startsWith("--")) {
|
|
61
|
+
if (args.schema !== void 0) {
|
|
62
|
+
throw new Error(`Unexpected argument "${arg}". markdown takes a single schema path.`);
|
|
63
|
+
}
|
|
64
|
+
args.schema = arg;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const equalsIndex = arg.indexOf("=");
|
|
68
|
+
if (equalsIndex !== -1) {
|
|
69
|
+
const name = arg.slice(2, equalsIndex);
|
|
70
|
+
const key2 = toCamelCase(name);
|
|
71
|
+
if (BOOLEAN_KEYS.has(key2))
|
|
72
|
+
throw new Error(`Flag "--${name}" is a switch and takes no value.`);
|
|
73
|
+
if (!VALUE_KEYS.has(key2))
|
|
74
|
+
throw new Error(`Unknown flag "--${name}".`);
|
|
75
|
+
assignValue(args, key2, arg.slice(equalsIndex + 1));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const flagName = arg.slice(2);
|
|
79
|
+
const key = toCamelCase(flagName);
|
|
80
|
+
if (BOOLEAN_KEYS.has(key)) {
|
|
81
|
+
args.table = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!VALUE_KEYS.has(key))
|
|
85
|
+
throw new Error(`Unknown flag "--${flagName}".`);
|
|
86
|
+
const value = argv[i + 1];
|
|
87
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
88
|
+
throw new Error(`Flag "--${flagName}" expects a value.`);
|
|
89
|
+
}
|
|
90
|
+
assignValue(args, key, value);
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
return args;
|
|
94
|
+
};
|
|
95
|
+
export {
|
|
96
|
+
parseMarkdownArgs
|
|
97
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunResult } from '../lint/run.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs `mjst markdown` over `argv`: renders a JSON Schema as documentation,
|
|
4
|
+
* either as the prose reference pages (the default) or as the HTML config table
|
|
5
|
+
* spliced into an existing markdown file (`--table`). Returns the exit code and
|
|
6
|
+
* the text it would print (rather than writing to the process streams) so tests
|
|
7
|
+
* can drive it in-process — the same shape as the other subcommands' `run`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const run: (argv: string[]) => Promise<RunResult>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
|
+
import { generateConfigTable, generateDocs } from "@amritk/generate-markdown";
|
|
3
|
+
import { MARKDOWN_HELP_TEXT } from "./help-text.js";
|
|
4
|
+
import { parseMarkdownArgs } from "./parse-markdown-args.js";
|
|
5
|
+
const usageError = (message) => ({ code: 2, stdout: "", stderr: `Error: ${message}
|
|
6
|
+
` });
|
|
7
|
+
const failure = (message) => ({ code: 1, stdout: "", stderr: `Error: ${message}
|
|
8
|
+
` });
|
|
9
|
+
const PAGE_FLAGS = ["outDir", "file", "title", "language", "layout", "sort", "headingLevel"];
|
|
10
|
+
const run = async (argv) => {
|
|
11
|
+
let args;
|
|
12
|
+
try {
|
|
13
|
+
args = parseMarkdownArgs(argv);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return usageError(error instanceof Error ? error.message : String(error));
|
|
16
|
+
}
|
|
17
|
+
if (args.help || argv.length === 0) {
|
|
18
|
+
return { code: 0, stdout: MARKDOWN_HELP_TEXT, stderr: "" };
|
|
19
|
+
}
|
|
20
|
+
if (args.schema === void 0) {
|
|
21
|
+
return usageError("A schema path is required. Usage: mjst markdown <schema> --out-dir <dir>");
|
|
22
|
+
}
|
|
23
|
+
if (args.table) {
|
|
24
|
+
const stray = PAGE_FLAGS.filter((flag) => args[flag] !== void 0);
|
|
25
|
+
if (stray.length > 0) {
|
|
26
|
+
return usageError(`--table renders one table into a single file, so it cannot be combined with ${stray.map((flag) => `--${flag.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`).join(", ")}. Drop --table to generate the prose reference pages.`);
|
|
27
|
+
}
|
|
28
|
+
} else if (args.readme !== void 0) {
|
|
29
|
+
return usageError("--readme names the file the config table is spliced into, so it requires --table.");
|
|
30
|
+
}
|
|
31
|
+
const schemaPath = resolve(args.schema);
|
|
32
|
+
const outDir = resolve(args.outDir ?? ".");
|
|
33
|
+
if (args.table) {
|
|
34
|
+
let readmePath;
|
|
35
|
+
try {
|
|
36
|
+
readmePath = await generateConfigTable({
|
|
37
|
+
schemaPath,
|
|
38
|
+
...args.readme !== void 0 ? { readmePath: resolve(args.readme) } : {}
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
42
|
+
}
|
|
43
|
+
return { code: 0, stdout: `Generated config table: ${relative(process.cwd(), readmePath)}
|
|
44
|
+
`, stderr: "" };
|
|
45
|
+
}
|
|
46
|
+
const options = {
|
|
47
|
+
schemaPath,
|
|
48
|
+
outDir,
|
|
49
|
+
file: args.file,
|
|
50
|
+
title: args.title,
|
|
51
|
+
language: args.language,
|
|
52
|
+
layout: args.layout,
|
|
53
|
+
sort: args.sort,
|
|
54
|
+
headingLevel: args.headingLevel
|
|
55
|
+
};
|
|
56
|
+
let files;
|
|
57
|
+
try {
|
|
58
|
+
files = await generateDocs(options);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
61
|
+
}
|
|
62
|
+
const stdout = [
|
|
63
|
+
...files.map((file) => `Generated: ${relative(process.cwd(), resolve(outDir, file.filename))}`),
|
|
64
|
+
"",
|
|
65
|
+
`Total files generated: ${files.length}`,
|
|
66
|
+
""
|
|
67
|
+
].join("\n");
|
|
68
|
+
return { code: 0, stdout, stderr: "" };
|
|
69
|
+
};
|
|
70
|
+
export {
|
|
71
|
+
run
|
|
72
|
+
};
|
package/dist/parse-cli-args.js
CHANGED
|
@@ -28,7 +28,8 @@ const VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
|
28
28
|
"typeSuffix",
|
|
29
29
|
"importExt",
|
|
30
30
|
"rootType",
|
|
31
|
-
"unknownKeys"
|
|
31
|
+
"unknownKeys",
|
|
32
|
+
"formats"
|
|
32
33
|
]);
|
|
33
34
|
const EXTERNAL_VALUE_KEYS = /* @__PURE__ */ new Set(["config"]);
|
|
34
35
|
const toCamelCase = (key) => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -103,6 +104,10 @@ const assignValue = (config, key, value) => {
|
|
|
103
104
|
config.importExt = parsed;
|
|
104
105
|
return true;
|
|
105
106
|
}
|
|
107
|
+
case "formats": {
|
|
108
|
+
config.formats = value === "all" ? "all" : value.split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
106
111
|
case "unknownKeys": {
|
|
107
112
|
if (!isUnknownKeysStrategy(value)) {
|
|
108
113
|
throw new Error(`Invalid --unknown-keys value "${value}". Expected one of: ${UNKNOWN_KEYS_STRATEGIES.join(", ")}.`);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The JSON type each config key accepts, mirroring `config.schema.json`.
|
|
3
|
-
* `boolean|string` is `banner`'s `"type": ["boolean", "string"]
|
|
3
|
+
* `boolean|string` is `banner`'s `"type": ["boolean", "string"]`, and
|
|
4
|
+
* `string|string[]` is `formats`' `"type": ["string", "array"]` — a union the
|
|
5
|
+
* label spells by its element type rather than by the bare word `array`, since
|
|
6
|
+
* "expected string|array" tells a reader less than "expected string|string[]".
|
|
4
7
|
*/
|
|
5
|
-
type ConfigValueType = 'string' | 'boolean' | 'boolean|string' | 'string[]';
|
|
8
|
+
type ConfigValueType = 'string' | 'boolean' | 'boolean|string' | 'string[]' | 'string|string[]';
|
|
6
9
|
/** One key's contract: its JSON type, plus the closed value set when it has one. */
|
|
7
10
|
type ConfigKeySpec = {
|
|
8
11
|
readonly type: ConfigValueType;
|
package/dist/validate-config.js
CHANGED
|
@@ -20,6 +20,7 @@ const CONFIG_KEYS = {
|
|
|
20
20
|
caseInsensitive: { type: "boolean" },
|
|
21
21
|
readonly: { type: "boolean" },
|
|
22
22
|
unknownKeys: { type: "string", enum: [...UNKNOWN_KEYS_STRATEGIES] },
|
|
23
|
+
formats: { type: "string|string[]" },
|
|
23
24
|
helpers: { type: "string", enum: ["package", "embedded"] },
|
|
24
25
|
typeSuffix: { type: "string" },
|
|
25
26
|
banner: { type: "boolean|string" },
|
|
@@ -48,6 +49,8 @@ const matchesType = (value, type) => {
|
|
|
48
49
|
return typeof value === "boolean" || typeof value === "string";
|
|
49
50
|
case "string[]":
|
|
50
51
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
52
|
+
case "string|string[]":
|
|
53
|
+
return typeof value === "string" || Array.isArray(value) && value.every((e) => typeof e === "string");
|
|
51
54
|
}
|
|
52
55
|
};
|
|
53
56
|
const validateConfig = (parsed, configPath) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/mjst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -49,14 +49,15 @@
|
|
|
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.
|
|
55
|
-
"@amritk/generate-examples": "^0.8.
|
|
56
|
-
"@amritk/generate-
|
|
57
|
-
"@amritk/generate-
|
|
58
|
-
"@amritk/
|
|
59
|
-
"@amritk/
|
|
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",
|
|
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",
|
|
60
61
|
"@amritk/resolve-refs": "^0.7.1",
|
|
61
62
|
"@amritk/yaml": "^0.7.2",
|
|
62
63
|
"fast-glob": "^3.3.2",
|
|
@@ -64,7 +65,6 @@
|
|
|
64
65
|
"yargs": "^17.7.2"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
|
-
"@amritk/generate-markdown": "^0.6.0",
|
|
68
68
|
"@types/yargs": "^17.0.33"
|
|
69
69
|
}
|
|
70
70
|
}
|