@amritk/mjst 0.16.1 → 0.17.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 +2 -1
- package/README.md +28 -8
- package/dist/cli-config.d.ts +9 -0
- package/dist/cli.js +88 -4
- package/dist/help-text.d.ts +1 -1
- package/dist/help-text.js +4 -1
- package/dist/lint/run.js +24 -4
- package/dist/load-asyncapi-document.d.ts +12 -0
- package/dist/load-asyncapi-document.js +91 -0
- package/dist/load-config.js +3 -1
- package/dist/load-schema.js +1 -17
- package/dist/parse-cli-args.js +11 -2
- package/dist/resolve-policy.d.ts +33 -0
- package/dist/resolve-policy.js +21 -0
- package/dist/validate-config.js +3 -1
- package/package.json +10 -9
package/AI.md
CHANGED
|
@@ -46,7 +46,8 @@ mjst compile-api ./routes.ts --out ./dist/handler.ts
|
|
|
46
46
|
|
|
47
47
|
`--schema` / `--schema-dir`, `--out-dir` / `--out-file`, `--input`,
|
|
48
48
|
`--validators`, `--examples`, `--types-only`, `--build`, `--strict`,
|
|
49
|
-
`--strip-unknown`, `--
|
|
49
|
+
`--strip-unknown`, `--unknown-keys count-keys|count-enumerable`, `--readonly`,
|
|
50
|
+
`--import-ext ts|js`, `--config <path>`,
|
|
50
51
|
`--resolve-remote` / `--allowed-hosts` (SSRF-guarded remote `$ref`s).
|
|
51
52
|
|
|
52
53
|
## Lint gotchas
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ yarn add -D @amritk/mjst
|
|
|
37
37
|
bun add -d @amritk/mjst
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
The package ships a `mjst` bin that runs under Node ≥ 20 (or Bun), so you can invoke it via `npx mjst`, `pnpm
|
|
40
|
+
The package ships a `mjst` bin that runs under Node ≥ 20 (or Bun), so you can invoke it via `npx mjst`, `pnpm exec mjst`, `yarn mjst`, `bunx mjst`, or as a script in `package.json`.
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
@@ -64,7 +64,7 @@ npx mjst --schema ./schema.json --out-file ./generated/schema.ts --types-only
|
|
|
64
64
|
### Recursive — a whole folder of schemas
|
|
65
65
|
|
|
66
66
|
Point `--schema-dir` at a directory of JSON Schemas and mjst generates parsers for every
|
|
67
|
-
`*.json` file it finds, mirroring the directory layout under `--out-dir`:
|
|
67
|
+
`*.json` file it finds (skipping `node_modules/` and dot-directories), mirroring the directory layout under `--out-dir`:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
npx mjst --schema-dir ./schemas --out-dir ./generated
|
|
@@ -188,7 +188,7 @@ npx mjst --config ./mjst.config.json
|
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
> [!NOTE]
|
|
191
|
-
> Validate your config against the
|
|
191
|
+
> Validate your config against the repository's JSON Schema: [`config.schema.json`](./config.schema.json) (not included in the npm package)
|
|
192
192
|
|
|
193
193
|
---
|
|
194
194
|
|
|
@@ -220,7 +220,7 @@ With no `-r`, mjst discovers a `.lint.{yaml,yml,json,js,mjs}` ruleset by walking
|
|
|
220
220
|
| Flag | Description |
|
|
221
221
|
| --- | --- |
|
|
222
222
|
| `<documents..>` | Files or globs to lint (positional). |
|
|
223
|
-
| `-r`, `--ruleset` | Path to a ruleset file. Defaults to `.lint.*` discovery. |
|
|
223
|
+
| `-r`, `--ruleset` | Path to a ruleset file, or a built-in preset (`oas`, `asyncapi`). Defaults to `.lint.*` discovery. |
|
|
224
224
|
| `-F`, `--fail-severity` | Minimum severity that fails the run: `error` (default), `warn`, `info`, `hint`. |
|
|
225
225
|
| `-D`, `--display-only-failures` | Only show findings at or above `--fail-severity`. |
|
|
226
226
|
| `--stdin-filepath` | Path to associate with piped input (labels findings and enables ruleset discovery). |
|
|
@@ -230,6 +230,7 @@ With no `-r`, mjst discovers a `.lint.{yaml,yml,json,js,mjs}` ruleset by walking
|
|
|
230
230
|
| `--allowed-hosts` | Restrict remote `$ref` fetches to these hosts (implies `--resolve-remote`). |
|
|
231
231
|
| `--allow-private-hosts` | Permit remote `$ref`s to private/loopback hosts (SSRF guard, off by default). |
|
|
232
232
|
| `--allowed-roots` | Extra directories a local `$ref` may resolve into, on top of the document's own (repeat the flag). |
|
|
233
|
+
| `--encoding` | Input encoding for the linted files (default `utf8`). |
|
|
233
234
|
| `-q`, `--quiet` | Suppress the findings report (the exit code still reflects findings). |
|
|
234
235
|
|
|
235
236
|
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` — as long as they stay inside the linted document's own directory; use `--allowed-roots` to let a document reach a shared sibling folder. Remote refs are only fetched when you opt in with `--resolve-remote` or `--allowed-hosts`.
|
|
@@ -250,7 +251,7 @@ npx mjst compile-api src/routes.ts --out dist/handler.js
|
|
|
250
251
|
npx mjst compile-api src/routes.ts --out dist/handler.js --options compile-options.json
|
|
251
252
|
```
|
|
252
253
|
|
|
253
|
-
Every export of the routes module that looks like a route contract (`method`, `path`, `responses`) is compiled; the generated module imports those contracts back through `--routes-import` (default: the relative path from the out file to the routes module). The routes module is imported at build time, so it must be loadable by the runtime running mjst — run via `bunx` for TypeScript sources, or under Node install a loader such as `tsx` (`node --import tsx`).
|
|
254
|
+
Every *named* export of the routes module that looks like a route contract (`method`, `path`, `responses`) is compiled (a `default` export is ignored, because the generated module imports each contract by name); the generated module imports those contracts back through `--routes-import` (default: the relative path from the out file to the routes module). The routes module is imported at build time, so it must be loadable by the runtime running mjst — run via `bunx` for TypeScript sources, or under Node install a loader such as `tsx` (`node --import tsx`).
|
|
254
255
|
|
|
255
256
|
| Flag | Description |
|
|
256
257
|
| --- | --- |
|
|
@@ -303,7 +304,7 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
303
304
|
<td align="center"><code>"json"</code></td>
|
|
304
305
|
</tr>
|
|
305
306
|
<tr>
|
|
306
|
-
<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. 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></td>
|
|
307
|
+
<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>
|
|
307
308
|
</tr>
|
|
308
309
|
<tr>
|
|
309
310
|
<td>📦 <code>export</code></td>
|
|
@@ -312,7 +313,7 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
312
313
|
<td align="center"></td>
|
|
313
314
|
</tr>
|
|
314
315
|
<tr>
|
|
315
|
-
<td colspan="4">Which export of the schema module to use when 'input' names a module format (anything but 'json' and '
|
|
316
|
+
<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>
|
|
316
317
|
</tr>
|
|
317
318
|
<tr>
|
|
318
319
|
<td>📁 <code>outDir</code></td>
|
|
@@ -423,6 +424,15 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
423
424
|
<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>
|
|
424
425
|
</tr>
|
|
425
426
|
<tr>
|
|
427
|
+
<td>🔢 <code>unknownKeys</code></td>
|
|
428
|
+
<td><code>--unknown-keys <strategy></code></td>
|
|
429
|
+
<td><code>string</code></td>
|
|
430
|
+
<td align="center"><code>"count-keys"</code></td>
|
|
431
|
+
</tr>
|
|
432
|
+
<tr>
|
|
433
|
+
<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>
|
|
434
|
+
</tr>
|
|
435
|
+
<tr>
|
|
426
436
|
<td>🧰 <code>helpers</code></td>
|
|
427
437
|
<td><code>--helpers <mode></code></td>
|
|
428
438
|
<td><code>string</code></td>
|
|
@@ -510,7 +520,7 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
510
520
|
<td align="center"></td>
|
|
511
521
|
</tr>
|
|
512
522
|
<tr>
|
|
513
|
-
<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, 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>
|
|
523
|
+
<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>
|
|
514
524
|
</tr>
|
|
515
525
|
</tbody>
|
|
516
526
|
</table>
|
|
@@ -559,6 +569,16 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
559
569
|
}
|
|
560
570
|
```
|
|
561
571
|
|
|
572
|
+
**Strict — throw on invalid input**
|
|
573
|
+
|
|
574
|
+
```json
|
|
575
|
+
{
|
|
576
|
+
"schema": "./schema.json",
|
|
577
|
+
"outDir": "./generated",
|
|
578
|
+
"strict": true
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
562
582
|
---
|
|
563
583
|
|
|
564
584
|
## Scripts
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SourceFormat } from '@amritk/adapters/source-format';
|
|
2
|
+
import type { UnknownKeysStrategy } from '@amritk/helpers/unknown-keys-strategy';
|
|
2
3
|
/**
|
|
3
4
|
* Configuration for the CLI tool.
|
|
4
5
|
* These properties map 1:1 with CLI flags and config file keys.
|
|
@@ -105,6 +106,14 @@ export type CliConfig = {
|
|
|
105
106
|
* path is unaffected.
|
|
106
107
|
*/
|
|
107
108
|
readonly caseInsensitive?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* How generated fast paths prove a closed object (`additionalProperties:
|
|
111
|
+
* false`) carries no undeclared key. `'count-keys'` (the default) compares
|
|
112
|
+
* `Object.keys(obj).length` and is the faster form on JavaScriptCore (Bun);
|
|
113
|
+
* `'count-enumerable'` counts with `for…in` and is the faster form on V8
|
|
114
|
+
* (Node). Applies to parsers and validators alike.
|
|
115
|
+
*/
|
|
116
|
+
readonly unknownKeys?: UnknownKeysStrategy;
|
|
108
117
|
/**
|
|
109
118
|
* Controls how generated parsers reference their runtime helpers.
|
|
110
119
|
* - `'package'`: emit `import ... from '@amritk/helpers/...'`.
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { readdir, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { extractAsyncApi, listMessageSchemas } from "@amritk/asyncapi";
|
|
6
7
|
import { buildSchema } from "@amritk/generate-parsers";
|
|
7
8
|
import { buildValidatorSchema } from "@amritk/generate-validators";
|
|
8
9
|
import { deriveRootTypeName } from "@amritk/helpers/derive-root-type-name";
|
|
@@ -13,6 +14,7 @@ import { detectHelpersMode } from "./detect-helpers-mode.js";
|
|
|
13
14
|
import { emitExamples } from "./emit-examples.js";
|
|
14
15
|
import { ensureOutputDir } from "./ensure-output-dir.js";
|
|
15
16
|
import { HELP_TEXT } from "./help-text.js";
|
|
17
|
+
import { loadAsyncApiDocument } from "./load-asyncapi-document.js";
|
|
16
18
|
import { loadConfig } from "./load-config.js";
|
|
17
19
|
import { loadSchema } from "./load-schema.js";
|
|
18
20
|
import { parseCliArgs } from "./parse-cli-args.js";
|
|
@@ -134,7 +136,7 @@ const findJsonSchemas = async (dir) => {
|
|
|
134
136
|
return results;
|
|
135
137
|
};
|
|
136
138
|
const runValidators = async (config, schema, rootTypeName, writer, validatorsSubDir) => {
|
|
137
|
-
const files = await buildValidatorSchema(schema, rootTypeName, config.typeSuffix);
|
|
139
|
+
const files = await buildValidatorSchema(schema, rootTypeName, config.typeSuffix, void 0, config.unknownKeys);
|
|
138
140
|
const staged = [];
|
|
139
141
|
for (const file of files) {
|
|
140
142
|
const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
|
|
@@ -150,7 +152,7 @@ const runSingle = async (config, schemaPath, outputDir) => {
|
|
|
150
152
|
const helpersMode = resolveHelpersMode(config, outputDir);
|
|
151
153
|
const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
|
|
152
154
|
console.log(`Root type: ${rootTypeName}`);
|
|
153
|
-
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
|
|
155
|
+
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
|
|
154
156
|
const writer = await createOutputWriter(outputDir, config.force);
|
|
155
157
|
const written = await commitOrDiscard(writer, async () => {
|
|
156
158
|
for (const file of files) {
|
|
@@ -179,7 +181,7 @@ const runSingleFile = async (config, schemaPath, outFilePath) => {
|
|
|
179
181
|
await ensureOutputDir(outputDir);
|
|
180
182
|
const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
|
|
181
183
|
console.log(`Root type: ${rootTypeName}`);
|
|
182
|
-
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? "package", void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
|
|
184
|
+
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? "package", void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
|
|
183
185
|
const combined = combineGeneratedFiles(files);
|
|
184
186
|
const writer = await createOutputWriter(outputDir, config.force);
|
|
185
187
|
const written = await commitOrDiscard(writer, async () => {
|
|
@@ -221,7 +223,7 @@ const runRecursive = async (config, schemaDir, outputDir) => {
|
|
|
221
223
|
const rootTypeName = deriveRootTypeName(schema, schemaBaseName(schemaFile));
|
|
222
224
|
console.log(`
|
|
223
225
|
${relPath} \u2192 ${relNoExt}/ (root type: ${rootTypeName})`);
|
|
224
|
-
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
|
|
226
|
+
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
|
|
225
227
|
for (const file of files) {
|
|
226
228
|
if (file.filename.startsWith("_helpers/")) {
|
|
227
229
|
sharedHelpers.set(file.filename, file.content);
|
|
@@ -253,6 +255,62 @@ ${relPath} \u2192 ${relNoExt}/ (root type: ${rootTypeName})`);
|
|
|
253
255
|
Total files generated: ${writtenTsFiles.length}`);
|
|
254
256
|
}
|
|
255
257
|
};
|
|
258
|
+
const runAsyncApi = async (config, documentPath, outputDir) => {
|
|
259
|
+
const document = await loadAsyncApiDocument(config, documentPath);
|
|
260
|
+
const model = extractAsyncApi(document);
|
|
261
|
+
const schemas = listMessageSchemas(model);
|
|
262
|
+
for (const issue of model.issues) {
|
|
263
|
+
console.warn(`Warning: ${issue.path}: ${issue.message}`);
|
|
264
|
+
}
|
|
265
|
+
if (schemas.length === 0) {
|
|
266
|
+
console.error(`Error: no generatable message schemas found in ${documentPath}. Every message either declares no payload/headers or uses a non-JSON-Schema schemaFormat (see warnings above).`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
console.log(`AsyncAPI ${model.version}: ${model.channels.length} channel(s), ${schemas.length} schema(s) to generate`);
|
|
270
|
+
await ensureOutputDir(outputDir);
|
|
271
|
+
const helpersMode = resolveHelpersMode(config, outputDir);
|
|
272
|
+
const sharedHelpers = /* @__PURE__ */ new Map();
|
|
273
|
+
const writer = await createOutputWriter(outputDir, config.force);
|
|
274
|
+
const exampleTasks = [];
|
|
275
|
+
const stageAll = async () => {
|
|
276
|
+
for (const extracted of schemas) {
|
|
277
|
+
const { subDir, rootTypeName, schema } = extracted;
|
|
278
|
+
const depth = subDir.split("/").filter(Boolean).length;
|
|
279
|
+
const helpersImportPrefix = "../".repeat(depth);
|
|
280
|
+
console.log(`
|
|
281
|
+
${subDir}/ (root type: ${rootTypeName})`);
|
|
282
|
+
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
|
|
283
|
+
for (const file of files) {
|
|
284
|
+
if (file.filename.startsWith("_helpers/")) {
|
|
285
|
+
sharedHelpers.set(file.filename, file.content);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
|
|
289
|
+
await writer.stage(join(subDir, file.filename), content);
|
|
290
|
+
}
|
|
291
|
+
if (config.validators) {
|
|
292
|
+
await runValidators(config, schema, rootTypeName, writer, join("validators", subDir));
|
|
293
|
+
}
|
|
294
|
+
if (config.examples)
|
|
295
|
+
exampleTasks.push({ schema, rootTypeName, subDir });
|
|
296
|
+
}
|
|
297
|
+
for (const [filename, content] of sharedHelpers) {
|
|
298
|
+
await writer.stage(filename, content);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const writtenTsFiles = await commitOrDiscard(writer, stageAll);
|
|
302
|
+
for (const filename of writtenTsFiles)
|
|
303
|
+
console.log(`Generated: ${filename}`);
|
|
304
|
+
if (exampleTasks.length > 0) {
|
|
305
|
+
await runExamples(config, outputDir, exampleTasks);
|
|
306
|
+
}
|
|
307
|
+
if (config.build) {
|
|
308
|
+
await buildOutput(outputDir, writtenTsFiles, config.typesOnly);
|
|
309
|
+
} else {
|
|
310
|
+
console.log(`
|
|
311
|
+
Total files generated: ${writtenTsFiles.length}`);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
256
314
|
const run = async () => {
|
|
257
315
|
const args = process.argv.slice(2);
|
|
258
316
|
if (args[0] === "lint") {
|
|
@@ -310,6 +368,24 @@ const run = async () => {
|
|
|
310
368
|
console.error("Error: --validators cannot be combined with --out-file. Use --out-dir to emit validator files.");
|
|
311
369
|
process.exit(1);
|
|
312
370
|
}
|
|
371
|
+
if (config.input === "asyncapi") {
|
|
372
|
+
if (config.schemaDir) {
|
|
373
|
+
console.error("Error: --input asyncapi reads a single document via --schema; it cannot be combined with --schema-dir.");
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
if (config.outFile) {
|
|
377
|
+
console.error("Error: --input asyncapi generates a directory of files per message; use --out-dir, not --out-file.");
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
if (config.rootType) {
|
|
381
|
+
console.error("Error: --root-type cannot be combined with --input asyncapi; each message derives its own root type.");
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
if (config.export) {
|
|
385
|
+
console.error("Error: --export applies to module inputs (typebox, zod, ...); an AsyncAPI document has no exports.");
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
313
389
|
if (config.outFile) {
|
|
314
390
|
if (!config.typesOnly) {
|
|
315
391
|
console.error("Error: --out-file currently supports only --types-only output. Add --types-only or use --out-dir.");
|
|
@@ -331,6 +407,14 @@ const run = async () => {
|
|
|
331
407
|
process.exit(1);
|
|
332
408
|
}
|
|
333
409
|
const outputDir = resolve(config.outDir);
|
|
410
|
+
if (config.input === "asyncapi") {
|
|
411
|
+
if (!config.schema) {
|
|
412
|
+
console.error("Error: --schema is required. Provide a path to an AsyncAPI document (JSON or YAML).");
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
await runAsyncApi(config, resolve(config.schema), outputDir);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
334
418
|
if (config.schemaDir) {
|
|
335
419
|
await runRecursive(config, resolve(config.schemaDir), outputDir);
|
|
336
420
|
return;
|
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 --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 --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force Overwrite pre-existing files in the output destination\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 --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\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 --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force Overwrite pre-existing files in the output destination\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";
|
package/dist/help-text.js
CHANGED
|
@@ -12,7 +12,8 @@ Subcommands (each with its own flags and --help):
|
|
|
12
12
|
Input:
|
|
13
13
|
--schema <path> Schema to process: a JSON Schema file, or a module when --input is set
|
|
14
14
|
--schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)
|
|
15
|
-
--input <format> Schema source format: json (default), typebox, zod, valibot, effect, avro
|
|
15
|
+
--input <format> Schema source format: json (default), typebox, zod, valibot, effect, avro,
|
|
16
|
+
or asyncapi (an AsyncAPI 2.x/3.0 document, JSON or YAML)
|
|
16
17
|
--export <name> Which export of the schema module to use when --input names a module format
|
|
17
18
|
|
|
18
19
|
Output:
|
|
@@ -35,6 +36,8 @@ Validation:
|
|
|
35
36
|
--strip-unknown Silently drop undeclared input keys at every nesting level
|
|
36
37
|
--case-insensitive Normalize mis-cased enum/const strings to their exact casing
|
|
37
38
|
--log-warnings console.warn on input keys not declared in the schema
|
|
39
|
+
--unknown-keys <s> How closed objects are proven free of undeclared keys:
|
|
40
|
+
count-keys (default, fastest on Bun) or count-enumerable (fastest on Node)
|
|
38
41
|
|
|
39
42
|
Reference resolution:
|
|
40
43
|
--resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)
|
package/dist/lint/run.js
CHANGED
|
@@ -13,6 +13,16 @@ const SEVERITY_BY_NAME = {
|
|
|
13
13
|
hint: DiagnosticSeverity.Hint
|
|
14
14
|
};
|
|
15
15
|
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
|
+
]);
|
|
16
26
|
const formatReport = (findings) => {
|
|
17
27
|
if (findings.length === 0)
|
|
18
28
|
return "No problems found\n";
|
|
@@ -51,7 +61,11 @@ const parseArgs = async (argv) => {
|
|
|
51
61
|
try {
|
|
52
62
|
const parsed = await yargs(argv).scriptName("lint").parserConfiguration({ "greedy-arrays": false }).strict().exitProcess(false).fail((message, error) => {
|
|
53
63
|
throw error ?? new Error(message);
|
|
54
|
-
}).usage("$0 [documents..]", "Lint JSON/YAML documents against a ruleset").positional("documents", { describe: "Documents or globs to lint", type: "string", array: true }).option("ruleset", {
|
|
64
|
+
}).usage("$0 [documents..]", "Lint JSON/YAML documents against a ruleset").positional("documents", { describe: "Documents or globs to lint", type: "string", array: true }).option("ruleset", {
|
|
65
|
+
alias: "r",
|
|
66
|
+
type: "string",
|
|
67
|
+
describe: "Path to a ruleset file, or a built-in preset: asyncapi, oas"
|
|
68
|
+
}).option("encoding", { type: "string", default: "utf8", describe: "Input encoding" }).option("fail-severity", { alias: "F", type: "string", default: "error", choices: Object.keys(SEVERITY_BY_NAME) }).option("display-only-failures", { alias: "D", type: "boolean", default: false }).option("verbose", { type: "boolean", default: false }).option("quiet", { alias: "q", type: "boolean", default: false }).option("stdin-filepath", {
|
|
55
69
|
type: "string",
|
|
56
70
|
describe: "Path to associate with stdin input (labels findings and enables ruleset discovery)"
|
|
57
71
|
}).option("concurrency", {
|
|
@@ -127,9 +141,15 @@ Run \`mjst lint --help\` for the supported flags.
|
|
|
127
141
|
let rulesetDefinition;
|
|
128
142
|
let rulesetBasePath;
|
|
129
143
|
if (parsed.ruleset) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
144
|
+
const preset = PRESET_RULESETS.get(parsed.ruleset);
|
|
145
|
+
if (preset) {
|
|
146
|
+
rulesetDefinition = await preset();
|
|
147
|
+
} else {
|
|
148
|
+
const definition = await loadRuleset(parsed.ruleset);
|
|
149
|
+
rulesetDefinition = definition;
|
|
150
|
+
rulesetBasePath = dirname(isAbsolute(parsed.ruleset) ? parsed.ruleset : resolve(process.cwd(), parsed.ruleset));
|
|
151
|
+
reportRulesetProblems(definition, parsed.ruleset);
|
|
152
|
+
}
|
|
133
153
|
}
|
|
134
154
|
const documents = parsed.documents ?? [];
|
|
135
155
|
const targets = await fg(documents, { dot: true, onlyFiles: true });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CliConfig } from './cli-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads and parses an AsyncAPI document (JSON or YAML) off disk. When the
|
|
4
|
+
* document carries cross-file or remote `$ref`s, the whole document is
|
|
5
|
+
* dereferenced with `@amritk/resolve-refs` under the same policy flags as JSON
|
|
6
|
+
* Schema input — offline by default, SSRF-guarded, confined to the document's
|
|
7
|
+
* directory. A document with only same-document `#/...` references (the common
|
|
8
|
+
* case) skips that pass entirely: the extractor follows them itself, and keeps
|
|
9
|
+
* `#/components/schemas/...` pointers *as names* so the generators emit one
|
|
10
|
+
* type per component instead of an inlined blob.
|
|
11
|
+
*/
|
|
12
|
+
export declare const loadAsyncApiDocument: (config: Partial<CliConfig>, documentPath: string) => Promise<unknown>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { assignKey } from "@amritk/helpers/assign-key";
|
|
4
|
+
import { readKey } from "@amritk/helpers/read-key";
|
|
5
|
+
import { resolveRefsFromFile } from "@amritk/resolve-refs";
|
|
6
|
+
import { parseDocument } from "@amritk/yaml";
|
|
7
|
+
import { hasExternalRefs } from "./has-external-refs.js";
|
|
8
|
+
import { buildResolveOptions, formatResolveErrors } from "./resolve-policy.js";
|
|
9
|
+
const parseDocumentText = (content, location) => {
|
|
10
|
+
const text = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
11
|
+
if (/\.json$/i.test(location))
|
|
12
|
+
return JSON.parse(text);
|
|
13
|
+
const doc = parseDocument(text);
|
|
14
|
+
if (doc.errors.length > 0) {
|
|
15
|
+
const details = doc.errors.slice(0, 5).map((error) => ` - ${error.message}`).join("\n");
|
|
16
|
+
throw new Error(`Failed to parse ${location} as YAML:
|
|
17
|
+
${details}`);
|
|
18
|
+
}
|
|
19
|
+
if (doc.warnings.some((warning) => warning.code === "MULTIPLE_DOCUMENTS")) {
|
|
20
|
+
throw new Error(`${location} contains multiple YAML documents; an AsyncAPI document must be a single-document file.`);
|
|
21
|
+
}
|
|
22
|
+
return doc.toJS();
|
|
23
|
+
};
|
|
24
|
+
const nodeAt = (root, pointer) => {
|
|
25
|
+
let node = root;
|
|
26
|
+
for (const segment of pointer) {
|
|
27
|
+
if (Array.isArray(node)) {
|
|
28
|
+
node = node[typeof segment === "number" ? segment : Number(segment)];
|
|
29
|
+
} else if (typeof node === "object" && node !== null) {
|
|
30
|
+
node = readKey(node, String(segment));
|
|
31
|
+
} else {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return node;
|
|
36
|
+
};
|
|
37
|
+
const canonicalizeAliases = (resolved, origins, rootLocation) => {
|
|
38
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const [copy, origin] of origins) {
|
|
40
|
+
if (origin.location !== rootLocation)
|
|
41
|
+
continue;
|
|
42
|
+
const target = nodeAt(resolved, origin.pointer);
|
|
43
|
+
if (typeof target === "object" && target !== null && target !== copy)
|
|
44
|
+
canonical.set(copy, target);
|
|
45
|
+
}
|
|
46
|
+
if (canonical.size === 0)
|
|
47
|
+
return;
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
const rewrite = (node) => {
|
|
50
|
+
if (typeof node !== "object" || node === null || seen.has(node))
|
|
51
|
+
return;
|
|
52
|
+
seen.add(node);
|
|
53
|
+
if (Array.isArray(node)) {
|
|
54
|
+
for (let index = 0; index < node.length; index++) {
|
|
55
|
+
const value = node[index];
|
|
56
|
+
const replacement = typeof value === "object" && value !== null ? canonical.get(value) : void 0;
|
|
57
|
+
if (replacement !== void 0)
|
|
58
|
+
node[index] = replacement;
|
|
59
|
+
else
|
|
60
|
+
rewrite(value);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const [key, value] of Object.entries(node)) {
|
|
65
|
+
const replacement = typeof value === "object" && value !== null ? canonical.get(value) : void 0;
|
|
66
|
+
if (replacement !== void 0)
|
|
67
|
+
assignKey(node, key, replacement);
|
|
68
|
+
else
|
|
69
|
+
rewrite(value);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
rewrite(resolved);
|
|
73
|
+
};
|
|
74
|
+
const loadAsyncApiDocument = async (config, documentPath) => {
|
|
75
|
+
const document = parseDocumentText(await readFile(documentPath, "utf-8"), documentPath);
|
|
76
|
+
if (!hasExternalRefs(document))
|
|
77
|
+
return document;
|
|
78
|
+
const { resolved, errors, origins } = await resolveRefsFromFile(documentPath, {
|
|
79
|
+
...buildResolveOptions(config, documentPath),
|
|
80
|
+
parse: parseDocumentText,
|
|
81
|
+
trackOrigins: true
|
|
82
|
+
});
|
|
83
|
+
if (errors.length > 0)
|
|
84
|
+
throw new Error(formatResolveErrors(documentPath, errors));
|
|
85
|
+
if (origins !== void 0)
|
|
86
|
+
canonicalizeAliases(resolved, origins, resolve(documentPath));
|
|
87
|
+
return resolved;
|
|
88
|
+
};
|
|
89
|
+
export {
|
|
90
|
+
loadAsyncApiDocument
|
|
91
|
+
};
|
package/dist/load-config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { isUnknownKeysStrategy } from "@amritk/helpers/unknown-keys-strategy";
|
|
3
4
|
import { validateConfig } from "./validate-config.js";
|
|
4
|
-
const SOURCE_FORMATS = ["json", "typebox", "zod", "valibot", "effect", "avro"];
|
|
5
|
+
const SOURCE_FORMATS = ["json", "typebox", "zod", "valibot", "effect", "avro", "asyncapi"];
|
|
5
6
|
const isSourceFormat = (value) => typeof value === "string" && SOURCE_FORMATS.includes(value);
|
|
6
7
|
const isHelpersMode = (value) => value === "package" || value === "embedded";
|
|
7
8
|
const isImportExt = (value) => value === "js" || value === "ts";
|
|
@@ -36,6 +37,7 @@ const loadConfig = async (configPath) => {
|
|
|
36
37
|
...typeof obj["stripUnknown"] === "boolean" && { stripUnknown: obj["stripUnknown"] },
|
|
37
38
|
...typeof obj["caseInsensitive"] === "boolean" && { caseInsensitive: obj["caseInsensitive"] },
|
|
38
39
|
...typeof obj["readonly"] === "boolean" && { readonly: obj["readonly"] },
|
|
40
|
+
...isUnknownKeysStrategy(obj["unknownKeys"]) && { unknownKeys: obj["unknownKeys"] },
|
|
39
41
|
...isHelpersMode(obj["helpers"]) && { helpers: obj["helpers"] },
|
|
40
42
|
...isImportExt(obj["importExt"]) && { importExt: obj["importExt"] },
|
|
41
43
|
...typeof obj["rootType"] === "string" && { rootType: obj["rootType"] },
|
package/dist/load-schema.js
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
3
2
|
import { getAdapter } from "@amritk/adapters/get-adapter";
|
|
4
3
|
import { resolveRefsFromFile } from "@amritk/resolve-refs";
|
|
5
|
-
import { withAllowedRootsHint } from "./allowed-roots-hint.js";
|
|
6
4
|
import { hasExternalRefs } from "./has-external-refs.js";
|
|
7
5
|
import { loadSchemaModule } from "./load-schema-module.js";
|
|
8
|
-
|
|
9
|
-
const remote = (config.resolveRemote ?? false) || (config.allowedHosts?.length ?? 0) > 0;
|
|
10
|
-
const extraRoots = (config.allowedRoots ?? []).map((root) => resolve(root));
|
|
11
|
-
return {
|
|
12
|
-
remote,
|
|
13
|
-
...extraRoots.length > 0 ? { allowedRoots: [dirname(resolve(schemaPath)), ...extraRoots] } : {},
|
|
14
|
-
...config.allowedHosts ? { allowedHosts: [...config.allowedHosts] } : {},
|
|
15
|
-
...config.allowPrivateHosts ? { allowPrivateHosts: config.allowPrivateHosts } : {}
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
const formatResolveErrors = (schemaPath, errors) => {
|
|
19
|
-
const details = errors.map((error) => ` - ${withAllowedRootsHint(error.message)}`).join("\n");
|
|
20
|
-
return `Failed to resolve $refs in ${schemaPath}:
|
|
21
|
-
${details}`;
|
|
22
|
-
};
|
|
6
|
+
import { buildResolveOptions, formatResolveErrors } from "./resolve-policy.js";
|
|
23
7
|
const loadSchema = async (config, schemaPath) => {
|
|
24
8
|
const inputFormat = config.input ?? "json";
|
|
25
9
|
if (inputFormat === "json") {
|
package/dist/parse-cli-args.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { isUnknownKeysStrategy, UNKNOWN_KEYS_STRATEGIES } from "@amritk/helpers/unknown-keys-strategy";
|
|
2
|
+
const SOURCE_FORMATS = ["json", "typebox", "zod", "valibot", "effect", "avro", "asyncapi"];
|
|
2
3
|
const LIST_KEYS = /* @__PURE__ */ new Set(["allowedHosts", "allowedRoots"]);
|
|
3
4
|
const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
|
|
4
5
|
"typesOnly",
|
|
@@ -24,7 +25,8 @@ const VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
|
24
25
|
"helpers",
|
|
25
26
|
"typeSuffix",
|
|
26
27
|
"importExt",
|
|
27
|
-
"rootType"
|
|
28
|
+
"rootType",
|
|
29
|
+
"unknownKeys"
|
|
28
30
|
]);
|
|
29
31
|
const EXTERNAL_VALUE_KEYS = /* @__PURE__ */ new Set(["config"]);
|
|
30
32
|
const toCamelCase = (key) => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -96,6 +98,13 @@ const assignValue = (config, key, value) => {
|
|
|
96
98
|
config.importExt = parsed;
|
|
97
99
|
return true;
|
|
98
100
|
}
|
|
101
|
+
case "unknownKeys": {
|
|
102
|
+
if (!isUnknownKeysStrategy(value)) {
|
|
103
|
+
throw new Error(`Invalid --unknown-keys value "${value}". Expected one of: ${UNKNOWN_KEYS_STRATEGIES.join(", ")}.`);
|
|
104
|
+
}
|
|
105
|
+
config.unknownKeys = value;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
99
108
|
default:
|
|
100
109
|
return false;
|
|
101
110
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ResolveError } from '@amritk/resolve-refs';
|
|
2
|
+
import type { CliConfig } from './cli-config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the `@amritk/resolve-refs` options from the CLI config, mirroring the
|
|
5
|
+
* safety posture of `mjst lint`: remote (`http(s)`) `$ref`s are refused unless
|
|
6
|
+
* opted into, a non-empty `allowedHosts` implies remote fetching, private/
|
|
7
|
+
* loopback hosts stay blocked as an SSRF guard unless explicitly permitted, and
|
|
8
|
+
* a local `$ref` may not leave the document's own directory unless
|
|
9
|
+
* `allowedRoots` names somewhere else.
|
|
10
|
+
*
|
|
11
|
+
* `allowedRoots` is passed only when the user asked for it, and it *adds* to the
|
|
12
|
+
* document's own directory rather than replacing it. The library option replaces
|
|
13
|
+
* the default outright, which would make `--allowed-roots ../common` quietly
|
|
14
|
+
* revoke the folder the document itself lives in — the one directory a user
|
|
15
|
+
* would never think to list. Entries are resolved against the process cwd (both
|
|
16
|
+
* from the flag and from a config file), matching how `schema` and `outDir` are
|
|
17
|
+
* treated in `cli.ts`.
|
|
18
|
+
*
|
|
19
|
+
* Shared by every document loader (plain JSON Schema, AsyncAPI) so one flag set
|
|
20
|
+
* means one policy.
|
|
21
|
+
*/
|
|
22
|
+
export declare const buildResolveOptions: (config: Partial<CliConfig>, documentPath: string) => {
|
|
23
|
+
remote: boolean;
|
|
24
|
+
allowedRoots?: string[];
|
|
25
|
+
allowedHosts?: string[];
|
|
26
|
+
allowPrivateHosts?: true;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Renders resolver failures into a single CLI error message. A confinement
|
|
30
|
+
* refusal is rewritten to name `--allowed-roots`, because the raw library text
|
|
31
|
+
* tells the user to set an option the CLI does not otherwise expose.
|
|
32
|
+
*/
|
|
33
|
+
export declare const formatResolveErrors: (documentPath: string, errors: readonly ResolveError[]) => string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { withAllowedRootsHint } from "./allowed-roots-hint.js";
|
|
3
|
+
const buildResolveOptions = (config, documentPath) => {
|
|
4
|
+
const remote = (config.resolveRemote ?? false) || (config.allowedHosts?.length ?? 0) > 0;
|
|
5
|
+
const extraRoots = (config.allowedRoots ?? []).map((root) => resolve(root));
|
|
6
|
+
return {
|
|
7
|
+
remote,
|
|
8
|
+
...extraRoots.length > 0 ? { allowedRoots: [dirname(resolve(documentPath)), ...extraRoots] } : {},
|
|
9
|
+
...config.allowedHosts ? { allowedHosts: [...config.allowedHosts] } : {},
|
|
10
|
+
...config.allowPrivateHosts ? { allowPrivateHosts: config.allowPrivateHosts } : {}
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
const formatResolveErrors = (documentPath, errors) => {
|
|
14
|
+
const details = errors.map((error) => ` - ${withAllowedRootsHint(error.message)}`).join("\n");
|
|
15
|
+
return `Failed to resolve $refs in ${documentPath}:
|
|
16
|
+
${details}`;
|
|
17
|
+
};
|
|
18
|
+
export {
|
|
19
|
+
buildResolveOptions,
|
|
20
|
+
formatResolveErrors
|
|
21
|
+
};
|
package/dist/validate-config.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readKey } from "@amritk/helpers/read-key";
|
|
2
|
+
import { UNKNOWN_KEYS_STRATEGIES } from "@amritk/helpers/unknown-keys-strategy";
|
|
2
3
|
const CONFIG_KEYS = {
|
|
3
4
|
schema: { type: "string" },
|
|
4
5
|
schemaDir: { type: "string" },
|
|
5
|
-
input: { type: "string", enum: ["json", "typebox", "zod", "valibot", "effect", "avro"] },
|
|
6
|
+
input: { type: "string", enum: ["json", "typebox", "zod", "valibot", "effect", "avro", "asyncapi"] },
|
|
6
7
|
export: { type: "string" },
|
|
7
8
|
outDir: { type: "string" },
|
|
8
9
|
outFile: { type: "string" },
|
|
@@ -16,6 +17,7 @@ const CONFIG_KEYS = {
|
|
|
16
17
|
stripUnknown: { type: "boolean" },
|
|
17
18
|
caseInsensitive: { type: "boolean" },
|
|
18
19
|
readonly: { type: "boolean" },
|
|
20
|
+
unknownKeys: { type: "string", enum: [...UNKNOWN_KEYS_STRATEGIES] },
|
|
19
21
|
helpers: { type: "string", enum: ["package", "embedded"] },
|
|
20
22
|
typeSuffix: { type: "string" },
|
|
21
23
|
banner: { type: "boolean|string" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/mjst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -49,13 +49,14 @@
|
|
|
49
49
|
"./package.json": "./package.json"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@amritk/adapters": "^0.
|
|
53
|
-
"@amritk/api": "^0.16.
|
|
54
|
-
"@amritk/
|
|
55
|
-
"@amritk/generate-
|
|
56
|
-
"@amritk/generate-
|
|
57
|
-
"@amritk/
|
|
58
|
-
"@amritk/
|
|
52
|
+
"@amritk/adapters": "^0.6.0",
|
|
53
|
+
"@amritk/api": "^0.16.3",
|
|
54
|
+
"@amritk/asyncapi": "^0.1.0",
|
|
55
|
+
"@amritk/generate-examples": "^0.8.2",
|
|
56
|
+
"@amritk/generate-parsers": "^0.21.0",
|
|
57
|
+
"@amritk/generate-validators": "^0.16.0",
|
|
58
|
+
"@amritk/helpers": "^0.19.0",
|
|
59
|
+
"@amritk/lint": "^0.5.3",
|
|
59
60
|
"@amritk/resolve-refs": "^0.7.1",
|
|
60
61
|
"@amritk/yaml": "^0.7.2",
|
|
61
62
|
"fast-glob": "^3.3.2",
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"yargs": "^17.7.2"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
|
-
"@amritk/generate-markdown": "^0.
|
|
67
|
+
"@amritk/generate-markdown": "^0.6.0",
|
|
67
68
|
"@types/yargs": "^17.0.33"
|
|
68
69
|
}
|
|
69
70
|
}
|