@amritk/mjst 0.10.0 → 0.12.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/README.md +126 -1
- package/dist/cli-config.d.ts +49 -0
- package/dist/cli.js +81 -18
- package/dist/emit-examples.d.ts +36 -0
- package/dist/emit-examples.js +29 -0
- package/dist/has-external-refs.d.ts +8 -0
- package/dist/has-external-refs.js +22 -0
- package/dist/help-text.d.ts +1 -1
- package/dist/help-text.js +8 -0
- package/dist/lint/resolver.d.ts +24 -0
- package/dist/lint/resolver.js +126 -0
- package/dist/lint/run.js +32 -0
- package/dist/load-config.js +6 -0
- package/dist/load-schema.d.ts +14 -0
- package/dist/load-schema.js +52 -0
- package/dist/parse-cli-args.js +47 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -85,6 +85,30 @@ imports from that single shared location. `--build` compiles the whole tree in p
|
|
|
85
85
|
schema's root type is named after the schema (its `title`, else its filename in PascalCase),
|
|
86
86
|
so `user.json` yields `User` / `parseUser` and `api/order.json` yields `Order` / `parseOrder`.
|
|
87
87
|
|
|
88
|
+
### Validators
|
|
89
|
+
|
|
90
|
+
Pass `--validators` to also emit runtime validation functions beside the parsers. For every
|
|
91
|
+
generated type `X` you get a `validateX(input)` — returning a rich `ValidationResult` with
|
|
92
|
+
JSON-Pointer error paths — and an `isX(input)` boolean type guard:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx mjst --schema ./schema.json --out-dir ./generated --validators
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The validators carry the same schema-derived filenames as the parsers, so they land in a
|
|
99
|
+
`validators/` subdirectory to avoid colliding:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
generated/
|
|
103
|
+
schema.ts, index.ts ← parsers + types
|
|
104
|
+
validators/
|
|
105
|
+
schema.ts, index.ts ← validateX / isX
|
|
106
|
+
validation-result.ts ← ValidationResult / ValidationError contract
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This works with `--schema-dir` too (the `validators/` tree mirrors the parser layout) and with
|
|
110
|
+
`--build`. It cannot be combined with `--types-only` or `--out-file`, which emit no runtime code.
|
|
111
|
+
|
|
88
112
|
### Running the generated output
|
|
89
113
|
|
|
90
114
|
By default mjst emits relative imports with the literal `.ts` extension (e.g.
|
|
@@ -107,6 +131,38 @@ since tsc cannot emit from `.ts` specifiers.
|
|
|
107
131
|
> giving `parseSpecPlan` / `validateSpecPlanShape`). Pass `--root-type <Name>` to override it for
|
|
108
132
|
> a single `--schema` run. Update any imports that referenced `Document` / `parseDocument`.
|
|
109
133
|
|
|
134
|
+
### External and remote `$ref`s
|
|
135
|
+
|
|
136
|
+
A schema can split its definitions across files. When a schema references another
|
|
137
|
+
document — `{ "$ref": "./address.json" }` or `{ "$ref": "./common.json#/$defs/id" }` —
|
|
138
|
+
mjst dereferences those cross-file targets before generating, inlining them into a
|
|
139
|
+
single schema so the generated parsers cover the whole shape:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx mjst --schema ./order.json --out-dir ./generated
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Same-document refs (`#/$defs/...`) are left untouched — the generator resolves those
|
|
146
|
+
itself into named type files, so nothing changes for schemas that only reference
|
|
147
|
+
themselves. Local cross-file refs resolve from disk with no extra flags. Remote
|
|
148
|
+
(`http(s)`) refs stay **offline by default**: a schema with a remote `$ref` fails
|
|
149
|
+
rather than making a network call unless you opt in.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Allow remote $ref fetches
|
|
153
|
+
npx mjst --schema ./order.json --out-dir ./generated --resolve-remote
|
|
154
|
+
|
|
155
|
+
# …restricted to specific hosts (implies --resolve-remote)
|
|
156
|
+
npx mjst --schema ./order.json --out-dir ./generated --allowed-hosts schemas.example.com
|
|
157
|
+
|
|
158
|
+
# …permitting private/loopback hosts (off by default as an SSRF guard)
|
|
159
|
+
npx mjst --schema ./order.json --out-dir ./generated --resolve-remote --allow-private-hosts
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Unresolvable references (a missing file, a refused host, a bad URL) fail the run with
|
|
163
|
+
the underlying reason rather than emitting a half-resolved schema. This works with
|
|
164
|
+
`--schema-dir` too — each schema resolves its own external references.
|
|
165
|
+
|
|
110
166
|
### Config file
|
|
111
167
|
|
|
112
168
|
```bash
|
|
@@ -151,8 +207,14 @@ With no `-r`, mjst discovers a `.lint.{yaml,yml,json,js,mjs}` ruleset by walking
|
|
|
151
207
|
| `-D`, `--display-only-failures` | Only show findings at or above `--fail-severity`. |
|
|
152
208
|
| `--stdin-filepath` | Path to associate with piped input (labels findings and enables ruleset discovery). |
|
|
153
209
|
| `--concurrency` | Max documents linted in parallel (default `8`). |
|
|
210
|
+
| `--no-resolve` | Skip `$ref` dereferencing; rules see the raw document. |
|
|
211
|
+
| `--resolve-remote` | Fetch `http(s)` `$ref`s while resolving (off by default; a lint run stays offline). |
|
|
212
|
+
| `--allowed-hosts` | Restrict remote `$ref` fetches to these hosts (implies `--resolve-remote`). |
|
|
213
|
+
| `--allow-private-hosts` | Permit remote `$ref`s to private/loopback hosts (SSRF guard, off by default). |
|
|
154
214
|
| `-q`, `--quiet` | Suppress the findings report (the exit code still reflects findings). |
|
|
155
215
|
|
|
216
|
+
By default `mjst lint` dereferences `$ref`, `$dynamicRef`/`$dynamicAnchor`, and `$recursiveRef`/`$recursiveAnchor` before running rules, so rules with `resolved: true` (the ruleset default) see through references. Internal and local cross-file refs resolve from disk — a finding on a cross-file node reports that file's own `line:column` — while remote refs are only fetched when you opt in with `--resolve-remote` or `--allowed-hosts`.
|
|
217
|
+
|
|
156
218
|
The exit code is `0` when no finding reaches `--fail-severity`, `1` when one does, and `2` on a usage error. Run `mjst lint --help` for the full list.
|
|
157
219
|
|
|
158
220
|
---
|
|
@@ -234,6 +296,24 @@ The exit code is `0` when no finding reaches `--fail-severity`, `1` when one doe
|
|
|
234
296
|
<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>
|
|
235
297
|
</tr>
|
|
236
298
|
<tr>
|
|
299
|
+
<td>✅ <code>validators</code></td>
|
|
300
|
+
<td><code>--validators</code></td>
|
|
301
|
+
<td><code>boolean</code></td>
|
|
302
|
+
<td align="center"><code>false</code></td>
|
|
303
|
+
</tr>
|
|
304
|
+
<tr>
|
|
305
|
+
<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>
|
|
306
|
+
</tr>
|
|
307
|
+
<tr>
|
|
308
|
+
<td>🎲 <code>examples</code></td>
|
|
309
|
+
<td><code>--examples</code></td>
|
|
310
|
+
<td><code>boolean</code></td>
|
|
311
|
+
<td align="center"><code>false</code></td>
|
|
312
|
+
</tr>
|
|
313
|
+
<tr>
|
|
314
|
+
<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>
|
|
315
|
+
</tr>
|
|
316
|
+
<tr>
|
|
237
317
|
<td>🔨 <code>build</code></td>
|
|
238
318
|
<td><code>--build</code></td>
|
|
239
319
|
<td><code>boolean</code></td>
|
|
@@ -270,6 +350,15 @@ The exit code is `0` when no finding reaches `--fail-severity`, `1` when one doe
|
|
|
270
350
|
<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>
|
|
271
351
|
</tr>
|
|
272
352
|
<tr>
|
|
353
|
+
<td>🔡 <code>caseInsensitive</code></td>
|
|
354
|
+
<td><code>--case-insensitive</code></td>
|
|
355
|
+
<td><code>boolean</code></td>
|
|
356
|
+
<td align="center"><code>false</code></td>
|
|
357
|
+
</tr>
|
|
358
|
+
<tr>
|
|
359
|
+
<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>
|
|
360
|
+
</tr>
|
|
361
|
+
<tr>
|
|
273
362
|
<td>🔒 <code>readonly</code></td>
|
|
274
363
|
<td><code>--readonly</code></td>
|
|
275
364
|
<td><code>boolean</code></td>
|
|
@@ -297,6 +386,15 @@ The exit code is `0` when no finding reaches `--fail-severity`, `1` when one doe
|
|
|
297
386
|
<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>
|
|
298
387
|
</tr>
|
|
299
388
|
<tr>
|
|
389
|
+
<td>🪧 <code>banner</code></td>
|
|
390
|
+
<td><code>--banner [text]</code></td>
|
|
391
|
+
<td><code>boolean | string</code></td>
|
|
392
|
+
<td align="center"><code>false</code></td>
|
|
393
|
+
</tr>
|
|
394
|
+
<tr>
|
|
395
|
+
<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>
|
|
396
|
+
</tr>
|
|
397
|
+
<tr>
|
|
300
398
|
<td>🔗 <code>importExt</code></td>
|
|
301
399
|
<td><code>--import-ext <ext></code></td>
|
|
302
400
|
<td><code>string</code></td>
|
|
@@ -315,13 +413,40 @@ The exit code is `0` when no finding reaches `--fail-severity`, `1` when one doe
|
|
|
315
413
|
<td colspan="4">Name for the root type of a single --schema run (e.g. 'Program' yields parseProgram / validateProgramShape). 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>
|
|
316
414
|
</tr>
|
|
317
415
|
<tr>
|
|
416
|
+
<td>🌐 <code>resolveRemote</code></td>
|
|
417
|
+
<td><code>--resolve-remote</code></td>
|
|
418
|
+
<td><code>boolean</code></td>
|
|
419
|
+
<td align="center"><code>false</code></td>
|
|
420
|
+
</tr>
|
|
421
|
+
<tr>
|
|
422
|
+
<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>
|
|
423
|
+
</tr>
|
|
424
|
+
<tr>
|
|
425
|
+
<td>🛡️ <code>allowedHosts</code></td>
|
|
426
|
+
<td><code>--allowed-hosts <hosts></code></td>
|
|
427
|
+
<td><code>array</code></td>
|
|
428
|
+
<td align="center"></td>
|
|
429
|
+
</tr>
|
|
430
|
+
<tr>
|
|
431
|
+
<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>
|
|
432
|
+
</tr>
|
|
433
|
+
<tr>
|
|
434
|
+
<td>🔓 <code>allowPrivateHosts</code></td>
|
|
435
|
+
<td><code>--allow-private-hosts</code></td>
|
|
436
|
+
<td><code>boolean</code></td>
|
|
437
|
+
<td align="center"><code>false</code></td>
|
|
438
|
+
</tr>
|
|
439
|
+
<tr>
|
|
440
|
+
<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>
|
|
441
|
+
</tr>
|
|
442
|
+
<tr>
|
|
318
443
|
<td>⚙️ <code>config</code></td>
|
|
319
444
|
<td><code>--config <path></code></td>
|
|
320
445
|
<td><code>string</code></td>
|
|
321
446
|
<td align="center"></td>
|
|
322
447
|
</tr>
|
|
323
448
|
<tr>
|
|
324
|
-
<td colspan="4">Path to a JSON config file. Keys match the option names in this schema (schema, schemaDir, outDir, outFile, typesOnly, build, logWarnings, strict, readonly, helpers, typeSuffix, importExt, rootType). CLI flags take precedence over config file values.<br><strong>Examples:</strong> <code>"./mjst.config.json"</code></td>
|
|
449
|
+
<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, logWarnings, strict, stripUnknown, caseInsensitive, readonly, helpers, typeSuffix, banner, importExt, rootType, resolveRemote, allowedHosts, allowPrivateHosts). CLI flags take precedence over config file values.<br><strong>Examples:</strong> <code>"./mjst.config.json"</code></td>
|
|
325
450
|
</tr>
|
|
326
451
|
</tbody>
|
|
327
452
|
</table>
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -36,6 +36,25 @@ export type CliConfig = {
|
|
|
36
36
|
* Useful when you only need the type shapes and do not need runtime validation.
|
|
37
37
|
*/
|
|
38
38
|
readonly typesOnly?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* When true, also emit validation functions alongside the parsers. For every
|
|
41
|
+
* generated type `X` the CLI writes a `validateX` (returning a rich
|
|
42
|
+
* `ValidationResult` with JSON-Pointer error paths) and an `isX` boolean type
|
|
43
|
+
* guard. The files land in a `validators/` subdirectory of the output so they
|
|
44
|
+
* never collide with the parser files, which share the same schema-derived
|
|
45
|
+
* names. Works with both `schema` and `schemaDir`. Incompatible with
|
|
46
|
+
* `typesOnly` and `outFile`, which produce no runtime code.
|
|
47
|
+
*/
|
|
48
|
+
readonly validators?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* When true, also emit test-data files for every schema: a `fast-check`
|
|
51
|
+
* arbitrary (`FooArbitrary`) that produces schema-valid values and a concrete
|
|
52
|
+
* `fooExample` value. The files are written into an `examples/` subdirectory of
|
|
53
|
+
* the output destination (mirroring the schema layout under `--schema-dir`) so
|
|
54
|
+
* they never collide with the parser output. The generated arbitraries import
|
|
55
|
+
* `fast-check`, which consumers must install as a (dev) dependency.
|
|
56
|
+
*/
|
|
57
|
+
readonly examples?: boolean;
|
|
39
58
|
/**
|
|
40
59
|
* When true, compile the generated TypeScript files to .js and .d.ts output.
|
|
41
60
|
* The .ts source files are removed after compilation.
|
|
@@ -61,6 +80,15 @@ export type CliConfig = {
|
|
|
61
80
|
* rather than strips in strict mode.
|
|
62
81
|
*/
|
|
63
82
|
readonly stripUnknown?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* When true, the generated coercing parsers normalize a mis-cased string to
|
|
85
|
+
* the exact casing of a declared `enum`/`const` member it matches
|
|
86
|
+
* case-insensitively (e.g. `hElLo` → `hello`) instead of coercing to the
|
|
87
|
+
* default. Coerce mode only — `strict` parsers still reject a casing
|
|
88
|
+
* mismatch. Correctly-cased input keeps the exact-match fast path, so the hot
|
|
89
|
+
* path is unaffected.
|
|
90
|
+
*/
|
|
91
|
+
readonly caseInsensitive?: boolean;
|
|
64
92
|
/**
|
|
65
93
|
* Controls how generated parsers reference their runtime helpers.
|
|
66
94
|
* - `'package'`: emit `import ... from '@amritk/helpers/...'`.
|
|
@@ -111,4 +139,25 @@ export type CliConfig = {
|
|
|
111
139
|
* Not supported with `schemaDir`, where each schema names its own root.
|
|
112
140
|
*/
|
|
113
141
|
readonly rootType?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Allow fetching `http(s)` `$ref`s while dereferencing a schema that contains
|
|
144
|
+
* cross-file or remote references. Defaults to `false`: generation stays
|
|
145
|
+
* offline unless asked, so a schema with a remote `$ref` fails rather than
|
|
146
|
+
* making a network call. A non-empty `allowedHosts` implies this.
|
|
147
|
+
*/
|
|
148
|
+
readonly resolveRemote?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* If set, only these hosts (e.g. `api.example.com`) may be fetched for remote
|
|
151
|
+
* `$ref`s. Restricting to an allow-list is itself an opt-in to remote fetching
|
|
152
|
+
* (it implies `resolveRemote`), and each listed host bypasses the private-host
|
|
153
|
+
* guard.
|
|
154
|
+
*/
|
|
155
|
+
readonly allowedHosts?: readonly string[];
|
|
156
|
+
/**
|
|
157
|
+
* Permit remote `$ref`s to private, loopback, and link-local hosts. Defaults
|
|
158
|
+
* to `false`: such targets are refused as a best-effort SSRF guard (notably the
|
|
159
|
+
* `169.254.169.254` cloud-metadata endpoint). An explicit `allowedHosts` entry
|
|
160
|
+
* always bypasses this guard.
|
|
161
|
+
*/
|
|
162
|
+
readonly allowPrivateHosts?: boolean;
|
|
114
163
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { mkdir, readdir,
|
|
3
|
+
import { mkdir, 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
|
-
import { getAdapter } from '@amritk/adapters/get-adapter';
|
|
7
6
|
import { buildSchema } from '@amritk/generate-parsers';
|
|
7
|
+
import { buildValidatorSchema } from '@amritk/generate-validators';
|
|
8
8
|
import { deriveRootTypeName } from '@amritk/helpers/derive-root-type-name';
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
import { combineGeneratedFiles } from './combine-files.js';
|
|
11
11
|
import { detectHelpersMode } from './detect-helpers-mode.js';
|
|
12
|
+
import { emitExamples } from './emit-examples.js';
|
|
12
13
|
import { HELP_TEXT } from './help-text.js';
|
|
13
14
|
import { loadConfig } from './load-config.js';
|
|
14
|
-
import {
|
|
15
|
+
import { loadSchema } from './load-schema.js';
|
|
15
16
|
import { parseCliArgs } from './parse-cli-args.js';
|
|
16
17
|
import { readVersion } from './read-version.js';
|
|
17
18
|
import { resolveImportExt } from './resolve-import-ext.js';
|
|
@@ -52,16 +53,6 @@ const extractConfigPath = (args) => {
|
|
|
52
53
|
}
|
|
53
54
|
return undefined;
|
|
54
55
|
};
|
|
55
|
-
/** Reads a JSON Schema off disk, or loads a module and converts it via its adapter. */
|
|
56
|
-
const loadSchema = async (config, schemaPath) => {
|
|
57
|
-
const inputFormat = config.input ?? 'json';
|
|
58
|
-
if (inputFormat === 'json') {
|
|
59
|
-
return JSON.parse(await readFile(schemaPath, 'utf-8'));
|
|
60
|
-
}
|
|
61
|
-
console.log(`Input format: ${inputFormat}`);
|
|
62
|
-
const source = await loadSchemaModule(schemaPath, config.export);
|
|
63
|
-
return getAdapter(inputFormat).toJSONSchema(source);
|
|
64
|
-
};
|
|
65
56
|
const DEFAULT_BANNER_TEXT = 'This file was auto-generated by @amritk/mjst.\n * Do not edit it manually — your changes will be overwritten on the next generation.';
|
|
66
57
|
const resolveBanner = (banner) => {
|
|
67
58
|
if (!banner)
|
|
@@ -75,6 +66,30 @@ const writeGeneratedFile = async (baseDir, filename, content) => {
|
|
|
75
66
|
await mkdir(dirname(filePath), { recursive: true });
|
|
76
67
|
await writeFile(filePath, content, 'utf-8');
|
|
77
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Emits fast-check arbitrary + example files for one schema (delegating to
|
|
71
|
+
* {@link emitExamples}) and logs each written path. The files land in an
|
|
72
|
+
* `examples/` subdirectory of `outputDir` so they never collide with the parser
|
|
73
|
+
* files, which share the same `<name>.ts` / `index.ts` names. `subDir` mirrors a
|
|
74
|
+
* schema's nested location under `--schema-dir`. The example sources are left out
|
|
75
|
+
* of `--build`: they import the `fast-check` peer dependency and are meant to be
|
|
76
|
+
* consumed by tests, not compiled alongside the runtime parsers.
|
|
77
|
+
*/
|
|
78
|
+
const runExamples = async (config, schema, rootTypeName, outputDir, subDir = '') => {
|
|
79
|
+
const written = await emitExamples({
|
|
80
|
+
schema: schema,
|
|
81
|
+
rootTypeName,
|
|
82
|
+
outputDir,
|
|
83
|
+
subDir,
|
|
84
|
+
bannerPrefix: config.banner ? resolveBanner(config.banner) : '',
|
|
85
|
+
// Spread conditionally: `exactOptionalPropertyTypes` rejects an explicit
|
|
86
|
+
// `undefined` for the optional `typeSuffix`.
|
|
87
|
+
...(config.typeSuffix !== undefined ? { typeSuffix: config.typeSuffix } : {}),
|
|
88
|
+
});
|
|
89
|
+
for (const path of written) {
|
|
90
|
+
console.log(`Generated example: ${path}`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
78
93
|
/**
|
|
79
94
|
* Compiles the generated `.ts` files to `.js`/`.d.ts` with a temporary tsconfig,
|
|
80
95
|
* then removes the intermediate sources. `tsFiles` are paths relative to `outputDir`.
|
|
@@ -145,6 +160,25 @@ const findJsonSchemas = async (dir) => {
|
|
|
145
160
|
}
|
|
146
161
|
return results;
|
|
147
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* Emits validator files (`validateX` / `isX`) for one schema into `validatorsSubDir`
|
|
165
|
+
* (a path relative to `outputDir`). The validators carry the same schema-derived
|
|
166
|
+
* filenames as the parsers, so they live in their own subdirectory to avoid
|
|
167
|
+
* colliding. Returns the written `.ts` paths relative to `outputDir` so the caller
|
|
168
|
+
* can fold them into a `--build` compilation.
|
|
169
|
+
*/
|
|
170
|
+
const runValidators = async (config, schema, rootTypeName, outputDir, validatorsSubDir) => {
|
|
171
|
+
const files = await buildValidatorSchema(schema, rootTypeName, config.typeSuffix);
|
|
172
|
+
const written = [];
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
|
|
175
|
+
const relFilename = join(validatorsSubDir, file.filename);
|
|
176
|
+
await writeGeneratedFile(outputDir, relFilename, content);
|
|
177
|
+
written.push(relFilename);
|
|
178
|
+
console.log(`Generated: ${relFilename}`);
|
|
179
|
+
}
|
|
180
|
+
return written;
|
|
181
|
+
};
|
|
148
182
|
/** Generates parsers for a single schema (the original one-schema-in, one-outDir-out flow). */
|
|
149
183
|
const runSingle = async (config, schemaPath, outputDir) => {
|
|
150
184
|
const schema = await loadSchema(config, schemaPath);
|
|
@@ -152,18 +186,24 @@ const runSingle = async (config, schemaPath, outputDir) => {
|
|
|
152
186
|
const helpersMode = resolveHelpersMode(config, outputDir);
|
|
153
187
|
const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
|
|
154
188
|
console.log(`Root type: ${rootTypeName}`);
|
|
155
|
-
const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, undefined, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config));
|
|
189
|
+
const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, undefined, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
|
|
156
190
|
for (const file of files) {
|
|
157
191
|
const isHelper = file.filename.startsWith('_helpers/');
|
|
158
192
|
const content = config.banner && !isHelper ? resolveBanner(config.banner) + file.content : file.content;
|
|
159
193
|
await writeGeneratedFile(outputDir, file.filename, content);
|
|
160
194
|
console.log(`Generated: ${file.filename}`);
|
|
161
195
|
}
|
|
196
|
+
const validatorFiles = config.validators
|
|
197
|
+
? await runValidators(config, schema, rootTypeName, outputDir, 'validators')
|
|
198
|
+
: [];
|
|
199
|
+
if (config.examples) {
|
|
200
|
+
await runExamples(config, schema, rootTypeName, outputDir);
|
|
201
|
+
}
|
|
162
202
|
if (config.build) {
|
|
163
|
-
await buildOutput(outputDir, files.map((f) => f.filename), config.typesOnly);
|
|
203
|
+
await buildOutput(outputDir, [...files.map((f) => f.filename), ...validatorFiles], config.typesOnly);
|
|
164
204
|
}
|
|
165
205
|
else {
|
|
166
|
-
console.log(`\nTotal files generated: ${files.length}`);
|
|
206
|
+
console.log(`\nTotal files generated: ${files.length + validatorFiles.length}`);
|
|
167
207
|
}
|
|
168
208
|
};
|
|
169
209
|
/**
|
|
@@ -177,10 +217,13 @@ const runSingleFile = async (config, schemaPath, outFilePath) => {
|
|
|
177
217
|
await mkdir(outputDir, { recursive: true });
|
|
178
218
|
const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
|
|
179
219
|
console.log(`Root type: ${rootTypeName}`);
|
|
180
|
-
const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? 'package', undefined, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config));
|
|
220
|
+
const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? 'package', undefined, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
|
|
181
221
|
const combined = combineGeneratedFiles(files);
|
|
182
222
|
await writeFile(outFilePath, config.banner ? resolveBanner(config.banner) + combined : combined, 'utf-8');
|
|
183
223
|
console.log(`Generated: ${relative(process.cwd(), outFilePath)}`);
|
|
224
|
+
if (config.examples) {
|
|
225
|
+
await runExamples(config, schema, rootTypeName, outputDir);
|
|
226
|
+
}
|
|
184
227
|
if (config.build) {
|
|
185
228
|
await buildOutput(outputDir, [basename(outFilePath)], config.typesOnly);
|
|
186
229
|
}
|
|
@@ -221,7 +264,7 @@ const runRecursive = async (config, schemaDir, outputDir) => {
|
|
|
221
264
|
const schema = await loadSchema(config, schemaFile);
|
|
222
265
|
const rootTypeName = deriveRootTypeName(schema, schemaBaseName(schemaFile));
|
|
223
266
|
console.log(`\n${relPath} → ${relNoExt}/ (root type: ${rootTypeName})`);
|
|
224
|
-
const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config));
|
|
267
|
+
const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
|
|
225
268
|
for (const file of files) {
|
|
226
269
|
if (file.filename.startsWith('_helpers/')) {
|
|
227
270
|
sharedHelpers.set(file.filename, file.content);
|
|
@@ -233,6 +276,16 @@ const runRecursive = async (config, schemaDir, outputDir) => {
|
|
|
233
276
|
writtenTsFiles.push(relativeFilename);
|
|
234
277
|
console.log(`Generated: ${relativeFilename}`);
|
|
235
278
|
}
|
|
279
|
+
if (config.validators) {
|
|
280
|
+
// Mirror the parser layout under a top-level `validators/` tree so each
|
|
281
|
+
// schema's validators sit beside their parser counterparts without sharing
|
|
282
|
+
// a filename.
|
|
283
|
+
const validatorFiles = await runValidators(config, schema, rootTypeName, outputDir, join('validators', relNoExt));
|
|
284
|
+
writtenTsFiles.push(...validatorFiles);
|
|
285
|
+
}
|
|
286
|
+
if (config.examples) {
|
|
287
|
+
await runExamples(config, schema, rootTypeName, outputDir, relNoExt);
|
|
288
|
+
}
|
|
236
289
|
}
|
|
237
290
|
for (const [filename, content] of sharedHelpers) {
|
|
238
291
|
await writeGeneratedFile(outputDir, filename, content);
|
|
@@ -293,6 +346,16 @@ const run = async () => {
|
|
|
293
346
|
console.error('Error: --import-ext ts cannot be combined with --build. Drop --build to keep runnable .ts sources.');
|
|
294
347
|
process.exit(1);
|
|
295
348
|
}
|
|
349
|
+
// Validators are runtime code (validateX / isX), so they have nothing to emit
|
|
350
|
+
// in the type-only flows.
|
|
351
|
+
if (config.validators && config.typesOnly) {
|
|
352
|
+
console.error('Error: --validators cannot be combined with --types-only, which emits no runtime code.');
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
if (config.validators && config.outFile) {
|
|
356
|
+
console.error('Error: --validators cannot be combined with --out-file. Use --out-dir to emit validator files.');
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
296
359
|
// Single-file output is the alternative to a directory of files.
|
|
297
360
|
if (config.outFile) {
|
|
298
361
|
if (!config.typesOnly) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/**
|
|
3
|
+
* Inputs for {@link emitExamples}. The schema and its root type name are the same
|
|
4
|
+
* ones handed to the parser generator, so the arbitraries/examples line up with
|
|
5
|
+
* the parsers that consume them.
|
|
6
|
+
*/
|
|
7
|
+
export type EmitExamplesOptions = {
|
|
8
|
+
readonly schema: JSONSchema;
|
|
9
|
+
readonly rootTypeName: string;
|
|
10
|
+
/** Output root; example files always land under `<outputDir>/examples/`. */
|
|
11
|
+
readonly outputDir: string;
|
|
12
|
+
/**
|
|
13
|
+
* Nested location of the schema relative to `--schema-dir` (e.g. `api/order`),
|
|
14
|
+
* mirrored beneath `examples/` so each schema's test data sits beside where its
|
|
15
|
+
* parsers were emitted. Defaults to `''` (single-schema output at the root).
|
|
16
|
+
*/
|
|
17
|
+
readonly subDir?: string;
|
|
18
|
+
/** Suffix appended to every `$ref`-derived type/arbitrary name. Defaults to `''`. */
|
|
19
|
+
readonly typeSuffix?: string;
|
|
20
|
+
/** Header comment (already wrapped in a JSDoc block) prepended to each file, or `''`. */
|
|
21
|
+
readonly bannerPrefix?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Emits fast-check arbitrary + concrete example files for one schema into an
|
|
25
|
+
* `examples/` subdirectory of `outputDir`.
|
|
26
|
+
*
|
|
27
|
+
* The dedicated subdirectory keeps the test-data output from colliding with the
|
|
28
|
+
* parser files, which otherwise share the same `<name>.ts` / `index.ts` names.
|
|
29
|
+
* Each generated file exports a `FooArbitrary` (a `fast-check` arbitrary that
|
|
30
|
+
* produces schema-valid values) and a static `fooExample` value; an `index.ts`
|
|
31
|
+
* barrel re-exports them. The arbitraries import `fast-check`, which consumers
|
|
32
|
+
* must install as a (dev) dependency.
|
|
33
|
+
*
|
|
34
|
+
* @returns The written file paths, relative to `outputDir`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const emitExamples: (options: EmitExamplesOptions) => Promise<string[]>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { buildExampleSchema } from '@amritk/generate-examples';
|
|
4
|
+
/**
|
|
5
|
+
* Emits fast-check arbitrary + concrete example files for one schema into an
|
|
6
|
+
* `examples/` subdirectory of `outputDir`.
|
|
7
|
+
*
|
|
8
|
+
* The dedicated subdirectory keeps the test-data output from colliding with the
|
|
9
|
+
* parser files, which otherwise share the same `<name>.ts` / `index.ts` names.
|
|
10
|
+
* Each generated file exports a `FooArbitrary` (a `fast-check` arbitrary that
|
|
11
|
+
* produces schema-valid values) and a static `fooExample` value; an `index.ts`
|
|
12
|
+
* barrel re-exports them. The arbitraries import `fast-check`, which consumers
|
|
13
|
+
* must install as a (dev) dependency.
|
|
14
|
+
*
|
|
15
|
+
* @returns The written file paths, relative to `outputDir`.
|
|
16
|
+
*/
|
|
17
|
+
export const emitExamples = async (options) => {
|
|
18
|
+
const { schema, rootTypeName, outputDir, subDir = '', typeSuffix, bannerPrefix = '' } = options;
|
|
19
|
+
const files = await buildExampleSchema(schema, rootTypeName, typeSuffix);
|
|
20
|
+
const exampleDir = join(outputDir, 'examples', subDir);
|
|
21
|
+
const written = [];
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const filePath = join(exampleDir, file.filename);
|
|
24
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
25
|
+
await writeFile(filePath, bannerPrefix + file.content, 'utf-8');
|
|
26
|
+
written.push(relative(outputDir, filePath));
|
|
27
|
+
}
|
|
28
|
+
return written;
|
|
29
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `data` contains a reference into another document (a `$ref` value that
|
|
3
|
+
* is not a same-document `#...` fragment). Same-document references resolve
|
|
4
|
+
* without a disk round-trip, so callers can skip the file-backed resolver — and
|
|
5
|
+
* preserve behavior that depends on those internal refs — unless an external
|
|
6
|
+
* (cross-file or remote) target exists.
|
|
7
|
+
*/
|
|
8
|
+
export declare const hasExternalRefs: (data: unknown) => boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The reference keywords whose value may point at another document (a non-`#` target). */
|
|
2
|
+
const REF_KEYWORDS = ['$ref', '$dynamicRef', '$recursiveRef'];
|
|
3
|
+
/**
|
|
4
|
+
* Whether `data` contains a reference into another document (a `$ref` value that
|
|
5
|
+
* is not a same-document `#...` fragment). Same-document references resolve
|
|
6
|
+
* without a disk round-trip, so callers can skip the file-backed resolver — and
|
|
7
|
+
* preserve behavior that depends on those internal refs — unless an external
|
|
8
|
+
* (cross-file or remote) target exists.
|
|
9
|
+
*/
|
|
10
|
+
export const hasExternalRefs = (data) => {
|
|
11
|
+
if (data === null || typeof data !== 'object')
|
|
12
|
+
return false;
|
|
13
|
+
if (Array.isArray(data))
|
|
14
|
+
return data.some(hasExternalRefs);
|
|
15
|
+
const obj = data;
|
|
16
|
+
for (const keyword of REF_KEYWORDS) {
|
|
17
|
+
const value = obj[keyword];
|
|
18
|
+
if (typeof value === 'string' && !value.startsWith('#'))
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return Object.values(obj).some(hasExternalRefs);
|
|
22
|
+
};
|
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\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\n --export <name> Which export of the schema module to use when --input is not json\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 --build Compile the generated files to .js/.d.ts (implies --import-ext js)\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 --log-warnings console.warn on input keys not declared in the schema\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\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\n --export <name> Which export of the schema module to use when --input is not json\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 --build Compile the generated files to .js/.d.ts (implies --import-ext js)\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\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\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,6 +21,8 @@ Output:
|
|
|
21
21
|
--out-dir <dir> Output directory for the generated files
|
|
22
22
|
--out-file <file> Single-file output instead of a directory (requires --types-only)
|
|
23
23
|
--types-only Generate type definitions only, without parser functions
|
|
24
|
+
--validators Also emit validateX/isX validators into a validators/ subdir
|
|
25
|
+
--examples Also emit fast-check arbitraries + example values under examples/
|
|
24
26
|
--build Compile the generated files to .js/.d.ts (implies --import-ext js)
|
|
25
27
|
--import-ext <ext> Extension on generated relative imports: ts (default) or js
|
|
26
28
|
--helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)
|
|
@@ -32,8 +34,14 @@ Output:
|
|
|
32
34
|
Validation:
|
|
33
35
|
--strict Throw on type/shape mismatches instead of coercing to defaults
|
|
34
36
|
--strip-unknown Silently drop undeclared input keys at every nesting level
|
|
37
|
+
--case-insensitive Normalize mis-cased enum/const strings to their exact casing
|
|
35
38
|
--log-warnings console.warn on input keys not declared in the schema
|
|
36
39
|
|
|
40
|
+
Reference resolution:
|
|
41
|
+
--resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)
|
|
42
|
+
--allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)
|
|
43
|
+
--allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)
|
|
44
|
+
|
|
37
45
|
Misc:
|
|
38
46
|
--config <path> JSON config file with the same keys; CLI flags take precedence
|
|
39
47
|
--version, -v Print the CLI version
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type LintResolver } from '@amritk/lint';
|
|
2
|
+
/** How `mjst lint` dereferences `$ref` (and `$dynamicRef`/`$recursiveRef`). */
|
|
3
|
+
export type ResolverOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Fetch http(s) `$ref`s. Defaults to `false`: a lint run should not make
|
|
6
|
+
* network calls unless asked. A non-empty `allowedHosts` implies `remote`.
|
|
7
|
+
*/
|
|
8
|
+
remote?: boolean;
|
|
9
|
+
/** If set, only these hosts may be fetched (and each bypasses the private-host guard). */
|
|
10
|
+
allowedHosts?: string[];
|
|
11
|
+
/** Permit remote refs to private/loopback/link-local hosts (off by default as an SSRF guard). */
|
|
12
|
+
allowPrivateHosts?: boolean;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Builds a {@link LintResolver} backed by `@amritk/resolve-refs`, dereferencing
|
|
16
|
+
* `$ref`, `$dynamicRef`/`$dynamicAnchor`, and `$recursiveRef`/`$recursiveAnchor`
|
|
17
|
+
* so rules with `resolved: true` (the ruleset default) see through references.
|
|
18
|
+
*
|
|
19
|
+
* Internal-only documents resolve in memory. A document with cross-file/remote
|
|
20
|
+
* refs is resolved from disk **only when its source file reads back exactly as
|
|
21
|
+
* the linted input** — so a piped document (`--stdin-filepath`) never resolves
|
|
22
|
+
* against a different on-disk file — with remote fetching gated by `options`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const createLintResolver: (options?: ResolverOptions) => LintResolver;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { createDocument, DiagnosticSeverity, resolveSourceOriginFromMap, } from '@amritk/lint';
|
|
4
|
+
import { resolveRefs, resolveRefsFromFile } from '@amritk/resolve-refs';
|
|
5
|
+
import { parse as parseYaml } from '@amritk/yaml';
|
|
6
|
+
import { hasExternalRefs } from '../has-external-refs.js';
|
|
7
|
+
const isRemote = (location) => /^https?:\/\//i.test(location);
|
|
8
|
+
/**
|
|
9
|
+
* Parses a referenced document by extension: YAML for `.yaml`/`.yml`, JSON for
|
|
10
|
+
* `.json`, and for anything else (extensionless, remote) tries JSON first, then
|
|
11
|
+
* YAML. YAML is a JSON superset, so this accepts every document the linter does.
|
|
12
|
+
*/
|
|
13
|
+
const parseDoc = (content, location) => {
|
|
14
|
+
if (/\.ya?ml$/i.test(location))
|
|
15
|
+
return parseYaml(content);
|
|
16
|
+
if (/\.json$/i.test(location))
|
|
17
|
+
return JSON.parse(content);
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(content);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return parseYaml(content);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Whether the file at `absolute` reads back byte-for-byte as `input` (the document we linted). */
|
|
26
|
+
const readsBackAs = (absolute, input) => {
|
|
27
|
+
try {
|
|
28
|
+
return readFileSync(absolute, 'utf8') === input;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Builds the {@link ISourceSet} the runner uses to map a finding on a resolved
|
|
36
|
+
* (dereferenced) node back to the file it came from. Origins from the resolver
|
|
37
|
+
* attribute each inlined node to its source document + path; `get` lazily parses
|
|
38
|
+
* each source file (with a position map) so findings on cross-file nodes report
|
|
39
|
+
* that file's own `line:column`. The root document is already parsed by the
|
|
40
|
+
* linter, so it is reused directly. Remote documents are not re-fetched here, so
|
|
41
|
+
* a finding inlined from a URL falls back to the root document's position.
|
|
42
|
+
*/
|
|
43
|
+
const buildSourceSet = (rootDocument, resolved, origins, rootLocation) => {
|
|
44
|
+
const documents = new Map([[rootLocation, rootDocument]]);
|
|
45
|
+
const originMap = origins ?? new Map();
|
|
46
|
+
const load = (location) => {
|
|
47
|
+
if (documents.has(location))
|
|
48
|
+
return documents.get(location);
|
|
49
|
+
let document;
|
|
50
|
+
if (location !== '' && !isRemote(location)) {
|
|
51
|
+
try {
|
|
52
|
+
document = createDocument(readFileSync(location, 'utf8'), { source: location });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
document = undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
documents.set(location, document);
|
|
59
|
+
return document;
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
get: load,
|
|
63
|
+
origin: (path) => resolveSourceOriginFromMap(resolved, originMap, rootLocation, path),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/** A zero-width range at the document start, for resolve errors with no recoverable position. */
|
|
67
|
+
const DOCUMENT_START = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
|
|
68
|
+
/**
|
|
69
|
+
* Maps resolver failures ({@link ResolveError}) into lint findings so an
|
|
70
|
+
* unresolvable `$ref` — a typo'd pointer, a missing file, a refused/failed
|
|
71
|
+
* remote fetch — surfaces as a diagnostic instead of being silently dropped. An
|
|
72
|
+
* error carrying a JSON path is anchored to that node's position in the root
|
|
73
|
+
* document (best effort, `closest`); otherwise the finding is document-level.
|
|
74
|
+
*/
|
|
75
|
+
const toFindings = (errors, root) => errors.map((error) => {
|
|
76
|
+
const location = error.path.length > 0 ? root.getLocationForJsonPath(error.path, true) : undefined;
|
|
77
|
+
const finding = {
|
|
78
|
+
code: 'unresolved-ref',
|
|
79
|
+
message: error.message,
|
|
80
|
+
path: error.path,
|
|
81
|
+
severity: DiagnosticSeverity.Error,
|
|
82
|
+
range: location?.range ?? DOCUMENT_START,
|
|
83
|
+
};
|
|
84
|
+
if (root.source !== undefined)
|
|
85
|
+
finding.source = root.source;
|
|
86
|
+
return finding;
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Builds a {@link LintResolver} backed by `@amritk/resolve-refs`, dereferencing
|
|
90
|
+
* `$ref`, `$dynamicRef`/`$dynamicAnchor`, and `$recursiveRef`/`$recursiveAnchor`
|
|
91
|
+
* so rules with `resolved: true` (the ruleset default) see through references.
|
|
92
|
+
*
|
|
93
|
+
* Internal-only documents resolve in memory. A document with cross-file/remote
|
|
94
|
+
* refs is resolved from disk **only when its source file reads back exactly as
|
|
95
|
+
* the linted input** — so a piped document (`--stdin-filepath`) never resolves
|
|
96
|
+
* against a different on-disk file — with remote fetching gated by `options`.
|
|
97
|
+
*/
|
|
98
|
+
export const createLintResolver = (options = {}) => {
|
|
99
|
+
const fromFileOptions = {
|
|
100
|
+
remote: options.remote ?? false,
|
|
101
|
+
...(options.allowedHosts ? { allowedHosts: options.allowedHosts } : {}),
|
|
102
|
+
...(options.allowPrivateHosts ? { allowPrivateHosts: options.allowPrivateHosts } : {}),
|
|
103
|
+
parse: parseDoc,
|
|
104
|
+
trackOrigins: true,
|
|
105
|
+
};
|
|
106
|
+
return async (document, { input }) => {
|
|
107
|
+
const { source } = document;
|
|
108
|
+
if (source && hasExternalRefs(document.data)) {
|
|
109
|
+
const absolute = resolvePath(source);
|
|
110
|
+
if (readsBackAs(absolute, input)) {
|
|
111
|
+
const { resolved, origins, errors } = await resolveRefsFromFile(absolute, fromFileOptions);
|
|
112
|
+
return {
|
|
113
|
+
resolved,
|
|
114
|
+
sources: buildSourceSet(document, resolved, origins, absolute),
|
|
115
|
+
...(errors.length > 0 ? { diagnostics: toFindings(errors, document) } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const { resolved, origins, errors } = resolveRefs(document.data, { trackOrigins: true });
|
|
120
|
+
return {
|
|
121
|
+
resolved,
|
|
122
|
+
sources: buildSourceSet(document, resolved, origins, ''),
|
|
123
|
+
...(errors.length > 0 ? { diagnostics: toFindings(errors, document) } : {}),
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
};
|
package/dist/lint/run.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, isAbsolute, resolve } from 'node:path';
|
|
|
3
3
|
import { DiagnosticSeverity, lintDocument, validateRuleset, } from '@amritk/lint';
|
|
4
4
|
import fg from 'fast-glob';
|
|
5
5
|
import yargs from 'yargs';
|
|
6
|
+
import { createLintResolver } from './resolver.js';
|
|
6
7
|
import { discoverRuleset, loadRuleset } from './ruleset-loader.js';
|
|
7
8
|
const SEVERITY_BY_NAME = {
|
|
8
9
|
error: DiagnosticSeverity.Error,
|
|
@@ -83,11 +84,40 @@ export const run = async (argv, options = {}) => {
|
|
|
83
84
|
type: 'number',
|
|
84
85
|
default: 8,
|
|
85
86
|
describe: 'Maximum number of documents to lint in parallel',
|
|
87
|
+
})
|
|
88
|
+
.option('resolve', {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
default: true,
|
|
91
|
+
describe: 'Dereference $ref / $dynamicRef / $recursiveRef before linting (use --no-resolve to disable)',
|
|
92
|
+
})
|
|
93
|
+
.option('resolve-remote', {
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
default: false,
|
|
96
|
+
describe: 'Allow fetching http(s) $refs while resolving (off by default; a lint run stays offline)',
|
|
97
|
+
})
|
|
98
|
+
.option('allowed-hosts', {
|
|
99
|
+
type: 'string',
|
|
100
|
+
array: true,
|
|
101
|
+
describe: 'Restrict remote $ref fetches to these hosts (implies --resolve-remote)',
|
|
102
|
+
})
|
|
103
|
+
.option('allow-private-hosts', {
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
default: false,
|
|
106
|
+
describe: 'Permit remote $refs to private/loopback hosts (SSRF guard, off by default)',
|
|
86
107
|
})
|
|
87
108
|
.help()
|
|
88
109
|
.alias('help', 'h')
|
|
89
110
|
.parse());
|
|
90
111
|
const failSeverity = SEVERITY_BY_NAME[parsed.failSeverity] ?? DiagnosticSeverity.Error;
|
|
112
|
+
// A non-empty --allowed-hosts is an explicit opt-in to remote fetching.
|
|
113
|
+
const allowRemote = parsed.resolveRemote || (parsed.allowedHosts?.length ?? 0) > 0;
|
|
114
|
+
const resolver = parsed.resolve
|
|
115
|
+
? createLintResolver({
|
|
116
|
+
remote: allowRemote,
|
|
117
|
+
...(parsed.allowedHosts ? { allowedHosts: parsed.allowedHosts } : {}),
|
|
118
|
+
allowPrivateHosts: parsed.allowPrivateHosts,
|
|
119
|
+
})
|
|
120
|
+
: undefined;
|
|
91
121
|
const reportRulesetProblems = (definition, label) => {
|
|
92
122
|
if (parsed.quiet)
|
|
93
123
|
return;
|
|
@@ -131,6 +161,7 @@ export const run = async (argv, options = {}) => {
|
|
|
131
161
|
const opts = {
|
|
132
162
|
...(definition ? { ruleset: definition } : {}),
|
|
133
163
|
...(basePath !== undefined ? { rulesetBasePath: basePath } : {}),
|
|
164
|
+
...(resolver && definition ? { resolve: resolver } : {}),
|
|
134
165
|
source: stdinPath ?? '<stdin>',
|
|
135
166
|
};
|
|
136
167
|
allResults.push(...(await lintDocument(content, opts)));
|
|
@@ -157,6 +188,7 @@ export const run = async (argv, options = {}) => {
|
|
|
157
188
|
? {
|
|
158
189
|
ruleset: definition,
|
|
159
190
|
...(basePath !== undefined ? { rulesetBasePath: basePath } : {}),
|
|
191
|
+
...(resolver ? { resolve: resolver } : {}),
|
|
160
192
|
source: file,
|
|
161
193
|
}
|
|
162
194
|
: { source: file };
|
package/dist/load-config.js
CHANGED
|
@@ -4,6 +4,7 @@ const SOURCE_FORMATS = ['json', 'typebox', 'zod', 'valibot', 'effect'];
|
|
|
4
4
|
const isSourceFormat = (value) => typeof value === 'string' && SOURCE_FORMATS.includes(value);
|
|
5
5
|
const isHelpersMode = (value) => value === 'package' || value === 'embedded';
|
|
6
6
|
const isImportExt = (value) => value === 'js' || value === 'ts';
|
|
7
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
7
8
|
/**
|
|
8
9
|
* Loads a JSON config file and returns the relevant CLI config properties.
|
|
9
10
|
* The config file should have the same keys as the CLI flags (schema, outDir).
|
|
@@ -24,10 +25,12 @@ export const loadConfig = async (configPath) => {
|
|
|
24
25
|
...(isSourceFormat(obj['input']) && { input: obj['input'] }),
|
|
25
26
|
...(typeof obj['export'] === 'string' && { export: obj['export'] }),
|
|
26
27
|
...(typeof obj['typesOnly'] === 'boolean' && { typesOnly: obj['typesOnly'] }),
|
|
28
|
+
...(typeof obj['examples'] === 'boolean' && { examples: obj['examples'] }),
|
|
27
29
|
...(typeof obj['build'] === 'boolean' && { build: obj['build'] }),
|
|
28
30
|
...(typeof obj['logWarnings'] === 'boolean' && { logWarnings: obj['logWarnings'] }),
|
|
29
31
|
...(typeof obj['strict'] === 'boolean' && { strict: obj['strict'] }),
|
|
30
32
|
...(typeof obj['stripUnknown'] === 'boolean' && { stripUnknown: obj['stripUnknown'] }),
|
|
33
|
+
...(typeof obj['caseInsensitive'] === 'boolean' && { caseInsensitive: obj['caseInsensitive'] }),
|
|
31
34
|
...(typeof obj['readonly'] === 'boolean' && { readonly: obj['readonly'] }),
|
|
32
35
|
...(isHelpersMode(obj['helpers']) && { helpers: obj['helpers'] }),
|
|
33
36
|
...(isImportExt(obj['importExt']) && { importExt: obj['importExt'] }),
|
|
@@ -35,5 +38,8 @@ export const loadConfig = async (configPath) => {
|
|
|
35
38
|
...(typeof obj['typeSuffix'] === 'string' && { typeSuffix: obj['typeSuffix'] }),
|
|
36
39
|
// `banner` is a boolean toggle or a custom header string, mirroring the CLI flag.
|
|
37
40
|
...((typeof obj['banner'] === 'boolean' || typeof obj['banner'] === 'string') && { banner: obj['banner'] }),
|
|
41
|
+
...(typeof obj['resolveRemote'] === 'boolean' && { resolveRemote: obj['resolveRemote'] }),
|
|
42
|
+
...(isStringArray(obj['allowedHosts']) && { allowedHosts: obj['allowedHosts'] }),
|
|
43
|
+
...(typeof obj['allowPrivateHosts'] === 'boolean' && { allowPrivateHosts: obj['allowPrivateHosts'] }),
|
|
38
44
|
};
|
|
39
45
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CliConfig } from './cli-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads a JSON Schema off disk, or loads a module and converts it via its adapter.
|
|
4
|
+
*
|
|
5
|
+
* For JSON input, a schema whose only references are same-document `#/...`
|
|
6
|
+
* pointers is parsed as-is — the generator resolves those internal `$ref`s
|
|
7
|
+
* itself into named type files, so inlining them here would collapse that
|
|
8
|
+
* structure. When a cross-file or remote `$ref` is present (which the generator
|
|
9
|
+
* cannot follow on its own), the schema is dereferenced with
|
|
10
|
+
* `@amritk/resolve-refs`, inlining every external target into a single document.
|
|
11
|
+
* Any resolve failures are surfaced as a CLI error rather than silently yielding
|
|
12
|
+
* a half-resolved schema.
|
|
13
|
+
*/
|
|
14
|
+
export declare const loadSchema: (config: Partial<CliConfig>, schemaPath: string) => Promise<unknown>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { getAdapter } from '@amritk/adapters/get-adapter';
|
|
3
|
+
import { resolveRefsFromFile } from '@amritk/resolve-refs';
|
|
4
|
+
import { hasExternalRefs } from './has-external-refs.js';
|
|
5
|
+
import { loadSchemaModule } from './load-schema-module.js';
|
|
6
|
+
/**
|
|
7
|
+
* Builds the `@amritk/resolve-refs` options from the CLI config, mirroring the
|
|
8
|
+
* safety posture of `mjst lint`: remote (`http(s)`) `$ref`s are refused unless
|
|
9
|
+
* opted into, a non-empty `allowedHosts` implies remote fetching, and private/
|
|
10
|
+
* loopback hosts stay blocked as an SSRF guard unless explicitly permitted.
|
|
11
|
+
*/
|
|
12
|
+
const buildResolveOptions = (config) => {
|
|
13
|
+
// A non-empty allow-list is itself an opt-in to remote fetching.
|
|
14
|
+
const remote = (config.resolveRemote ?? false) || (config.allowedHosts?.length ?? 0) > 0;
|
|
15
|
+
return {
|
|
16
|
+
remote,
|
|
17
|
+
...(config.allowedHosts ? { allowedHosts: [...config.allowedHosts] } : {}),
|
|
18
|
+
...(config.allowPrivateHosts ? { allowPrivateHosts: config.allowPrivateHosts } : {}),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/** Renders resolver failures into a single CLI error message. */
|
|
22
|
+
const formatResolveErrors = (schemaPath, errors) => {
|
|
23
|
+
const details = errors.map((error) => ` - ${error.message}`).join('\n');
|
|
24
|
+
return `Failed to resolve $refs in ${schemaPath}:\n${details}`;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Reads a JSON Schema off disk, or loads a module and converts it via its adapter.
|
|
28
|
+
*
|
|
29
|
+
* For JSON input, a schema whose only references are same-document `#/...`
|
|
30
|
+
* pointers is parsed as-is — the generator resolves those internal `$ref`s
|
|
31
|
+
* itself into named type files, so inlining them here would collapse that
|
|
32
|
+
* structure. When a cross-file or remote `$ref` is present (which the generator
|
|
33
|
+
* cannot follow on its own), the schema is dereferenced with
|
|
34
|
+
* `@amritk/resolve-refs`, inlining every external target into a single document.
|
|
35
|
+
* Any resolve failures are surfaced as a CLI error rather than silently yielding
|
|
36
|
+
* a half-resolved schema.
|
|
37
|
+
*/
|
|
38
|
+
export const loadSchema = async (config, schemaPath) => {
|
|
39
|
+
const inputFormat = config.input ?? 'json';
|
|
40
|
+
if (inputFormat === 'json') {
|
|
41
|
+
const data = JSON.parse(await readFile(schemaPath, 'utf-8'));
|
|
42
|
+
if (!hasExternalRefs(data))
|
|
43
|
+
return data;
|
|
44
|
+
const { resolved, errors } = await resolveRefsFromFile(schemaPath, buildResolveOptions(config));
|
|
45
|
+
if (errors.length > 0)
|
|
46
|
+
throw new Error(formatResolveErrors(schemaPath, errors));
|
|
47
|
+
return resolved;
|
|
48
|
+
}
|
|
49
|
+
console.log(`Input format: ${inputFormat}`);
|
|
50
|
+
const source = await loadSchemaModule(schemaPath, config.export);
|
|
51
|
+
return getAdapter(inputFormat).toJSONSchema(source);
|
|
52
|
+
};
|
package/dist/parse-cli-args.js
CHANGED
|
@@ -2,11 +2,16 @@ const SOURCE_FORMATS = ['json', 'typebox', 'zod', 'valibot', 'effect'];
|
|
|
2
2
|
// Boolean flags toggle on by presence and accept `--flag=false` to opt out.
|
|
3
3
|
const BOOLEAN_KEYS = new Set([
|
|
4
4
|
'typesOnly',
|
|
5
|
+
'validators',
|
|
6
|
+
'examples',
|
|
5
7
|
'build',
|
|
6
8
|
'logWarnings',
|
|
7
9
|
'strict',
|
|
8
10
|
'stripUnknown',
|
|
11
|
+
'caseInsensitive',
|
|
9
12
|
'readonly',
|
|
13
|
+
'resolveRemote',
|
|
14
|
+
'allowPrivateHosts',
|
|
10
15
|
]);
|
|
11
16
|
// Value flags consume the following argument (or `--flag=value`).
|
|
12
17
|
const VALUE_KEYS = new Set([
|
|
@@ -39,6 +44,20 @@ const parseImportExtValue = (value) => {
|
|
|
39
44
|
return undefined;
|
|
40
45
|
};
|
|
41
46
|
const parseInputValue = (value) => SOURCE_FORMATS.includes(value) ? value : undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Appends the hosts in `value` (comma-separated) onto `allowedHosts`. Repeating
|
|
49
|
+
* `--allowed-hosts` accumulates, so `--allowed-hosts a.com --allowed-hosts b.com`
|
|
50
|
+
* and `--allowed-hosts a.com,b.com` are equivalent. Blank entries are dropped.
|
|
51
|
+
*/
|
|
52
|
+
const appendAllowedHosts = (config, value) => {
|
|
53
|
+
const hosts = value
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((host) => host.trim())
|
|
56
|
+
.filter((host) => host.length > 0);
|
|
57
|
+
if (hosts.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
config.allowedHosts = [...(config.allowedHosts ?? []), ...hosts];
|
|
60
|
+
};
|
|
42
61
|
/** Assigns a value-flag onto the config. Returns false for unknown keys. */
|
|
43
62
|
const assignValue = (config, key, value) => {
|
|
44
63
|
switch (key) {
|
|
@@ -97,6 +116,12 @@ const assignBoolean = (config, key, value) => {
|
|
|
97
116
|
case 'typesOnly':
|
|
98
117
|
config.typesOnly = value;
|
|
99
118
|
return true;
|
|
119
|
+
case 'validators':
|
|
120
|
+
config.validators = value;
|
|
121
|
+
return true;
|
|
122
|
+
case 'examples':
|
|
123
|
+
config.examples = value;
|
|
124
|
+
return true;
|
|
100
125
|
case 'build':
|
|
101
126
|
config.build = value;
|
|
102
127
|
return true;
|
|
@@ -109,9 +134,18 @@ const assignBoolean = (config, key, value) => {
|
|
|
109
134
|
case 'stripUnknown':
|
|
110
135
|
config.stripUnknown = value;
|
|
111
136
|
return true;
|
|
137
|
+
case 'caseInsensitive':
|
|
138
|
+
config.caseInsensitive = value;
|
|
139
|
+
return true;
|
|
112
140
|
case 'readonly':
|
|
113
141
|
config.readonly = value;
|
|
114
142
|
return true;
|
|
143
|
+
case 'resolveRemote':
|
|
144
|
+
config.resolveRemote = value;
|
|
145
|
+
return true;
|
|
146
|
+
case 'allowPrivateHosts':
|
|
147
|
+
config.allowPrivateHosts = value;
|
|
148
|
+
return true;
|
|
115
149
|
default:
|
|
116
150
|
return false;
|
|
117
151
|
}
|
|
@@ -139,6 +173,9 @@ export const parseCliArgs = (args) => {
|
|
|
139
173
|
// --banner=false → false, --banner=true → true, --banner=<text> → custom string
|
|
140
174
|
config.banner = value === 'false' ? false : value === 'true' ? true : value;
|
|
141
175
|
}
|
|
176
|
+
else if (key === 'allowedHosts') {
|
|
177
|
+
appendAllowedHosts(config, value);
|
|
178
|
+
}
|
|
142
179
|
else if (BOOLEAN_KEYS.has(key)) {
|
|
143
180
|
assignBoolean(config, key, value !== 'false');
|
|
144
181
|
}
|
|
@@ -168,6 +205,16 @@ export const parseCliArgs = (args) => {
|
|
|
168
205
|
}
|
|
169
206
|
continue;
|
|
170
207
|
}
|
|
208
|
+
// --allowed-hosts takes a value (comma-separated) and accumulates when repeated.
|
|
209
|
+
if (key === 'allowedHosts') {
|
|
210
|
+
const value = args[i + 1];
|
|
211
|
+
if (value === undefined || value.startsWith('--')) {
|
|
212
|
+
throw new Error(`Flag "--${flagName}" expects a value.`);
|
|
213
|
+
}
|
|
214
|
+
appendAllowedHosts(config, value);
|
|
215
|
+
i++;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
171
218
|
// Boolean flag: presence alone enables it, no value needed
|
|
172
219
|
if (BOOLEAN_KEYS.has(key)) {
|
|
173
220
|
assignBoolean(config, key, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/mjst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,11 +41,15 @@
|
|
|
41
41
|
"generate-readme": "bun run ./scripts/generate-readme.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@amritk/adapters": "0.
|
|
45
|
-
"@amritk/generate-
|
|
44
|
+
"@amritk/adapters": "0.3.0",
|
|
45
|
+
"@amritk/generate-examples": "0.5.0",
|
|
46
|
+
"@amritk/generate-parsers": "0.16.1",
|
|
47
|
+
"@amritk/generate-validators": "0.11.6",
|
|
46
48
|
"@amritk/generate-markdown": "0.4.1",
|
|
47
|
-
"@amritk/helpers": "0.
|
|
48
|
-
"@amritk/lint": "0.
|
|
49
|
+
"@amritk/helpers": "0.13.0",
|
|
50
|
+
"@amritk/lint": "0.3.0",
|
|
51
|
+
"@amritk/resolve-refs": "0.4.0",
|
|
52
|
+
"@amritk/yaml": "0.3.0",
|
|
49
53
|
"fast-glob": "^3.3.2",
|
|
50
54
|
"yargs": "^17.7.2"
|
|
51
55
|
},
|