@amritk/mjst 0.17.0 → 0.19.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 +15 -4
- package/README.md +19 -1
- package/dist/cli-config.d.ts +24 -9
- package/dist/cli.js +64 -7
- package/dist/create-output-writer.d.ts +19 -17
- package/dist/create-output-writer.js +3 -27
- package/dist/detect-helpers-mode.d.ts +3 -8
- package/dist/detect-helpers-mode.js +2 -29
- package/dist/emit-examples.d.ts +3 -4
- package/dist/emit-message-contracts.d.ts +47 -0
- package/dist/emit-message-contracts.js +97 -0
- package/dist/help-text.d.ts +1 -1
- package/dist/help-text.js +3 -1
- package/dist/is-dependency-declared.d.ts +17 -0
- package/dist/is-dependency-declared.js +31 -0
- package/dist/load-config.js +2 -0
- package/dist/parse-cli-args.js +8 -0
- package/dist/validate-config.js +2 -0
- package/package.json +7 -7
package/AI.md
CHANGED
|
@@ -41,14 +41,25 @@ mjst compile-api ./routes.ts --out ./dist/handler.ts
|
|
|
41
41
|
point `--schema` at a JS/TS **module**; `avro` points it at a `.avsc` JSON
|
|
42
42
|
document, which is read as data (no `import()`, so `--export` does not
|
|
43
43
|
apply).
|
|
44
|
+
6. **`--input asyncapi`** reads an AsyncAPI 2.x/3.0 document (JSON or YAML) and
|
|
45
|
+
generates from every message payload/headers schema, one subtree per message.
|
|
46
|
+
It rejects `--schema-dir`, `--out-file`, `--root-type`, and `--export`.
|
|
47
|
+
7. **`--message-contracts`** (AsyncAPI only) additionally writes
|
|
48
|
+
`contracts/<channel>.ts` — a `defineMessages({ ... })` per channel, plus a
|
|
49
|
+
barrel. Those files import **`@amritk/api`**, which the *consuming* project
|
|
50
|
+
installs; the CLI does not put it there, and `--build` leaves the contracts
|
|
51
|
+
as `.ts` for that reason. Incompatible with `--types-only`. Use
|
|
52
|
+
`--discriminator <prop>` when the document tags frames with something other
|
|
53
|
+
than `type`; a channel carrying `x-mjst: { discriminator }` keeps its own.
|
|
44
54
|
|
|
45
55
|
## Common flags
|
|
46
56
|
|
|
47
57
|
`--schema` / `--schema-dir`, `--out-dir` / `--out-file`, `--input`,
|
|
48
|
-
`--validators`, `--examples`, `--
|
|
49
|
-
`--
|
|
50
|
-
`--
|
|
51
|
-
`--
|
|
58
|
+
`--validators`, `--examples`, `--message-contracts`, `--discriminator`,
|
|
59
|
+
`--types-only`, `--build`, `--strict`, `--strip-unknown`,
|
|
60
|
+
`--unknown-keys count-keys|count-enumerable`, `--readonly`,
|
|
61
|
+
`--import-ext ts|js`, `--config <path>`, `--resolve-remote` /
|
|
62
|
+
`--allowed-hosts` (SSRF-guarded remote `$ref`s).
|
|
52
63
|
|
|
53
64
|
## Lint gotchas
|
|
54
65
|
|
package/README.md
CHANGED
|
@@ -361,6 +361,24 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
361
361
|
<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>
|
|
362
362
|
</tr>
|
|
363
363
|
<tr>
|
|
364
|
+
<td>🔌 <code>messageContracts</code></td>
|
|
365
|
+
<td><code>--message-contracts</code></td>
|
|
366
|
+
<td><code>boolean</code></td>
|
|
367
|
+
<td align="center"><code>false</code></td>
|
|
368
|
+
</tr>
|
|
369
|
+
<tr>
|
|
370
|
+
<td colspan="4">Also emit a @amritk/api channel contract per AsyncAPI channel into a contracts/ subdirectory: one defineMessages({ ... }) module naming every message that channel carries — keyed by the value its discriminator property carries on the wire, with that property stripped out of each payload because the runtime removes it before validating — plus a barrel re-exporting them. Requires input 'asyncapi', which is the only input that has channels and operations to project from. Incompatible with typesOnly: a contract is a runtime value, not a type. The generated modules import @amritk/api, which the consuming project must install; it is not a dependency of the generated parsers.</td>
|
|
371
|
+
</tr>
|
|
372
|
+
<tr>
|
|
373
|
+
<td>🏷️ <code>discriminator</code></td>
|
|
374
|
+
<td><code>--discriminator <prop></code></td>
|
|
375
|
+
<td><code>string</code></td>
|
|
376
|
+
<td align="center"><code>"type"</code></td>
|
|
377
|
+
</tr>
|
|
378
|
+
<tr>
|
|
379
|
+
<td colspan="4">Property name that says which message a frame is, for the contracts messageContracts emits. Defaults to 'type', matching @amritk/api. A channel that declares its own with 'x-mjst': { 'discriminator': '...' } keeps it — one flag covers a whole run, and a run may span channels that disagree. Only meaningful together with messageContracts.<br><strong>Examples:</strong> <code>"event"</code></td>
|
|
380
|
+
</tr>
|
|
381
|
+
<tr>
|
|
364
382
|
<td>🔨 <code>build</code></td>
|
|
365
383
|
<td><code>--build</code></td>
|
|
366
384
|
<td><code>boolean</code></td>
|
|
@@ -376,7 +394,7 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
|
|
|
376
394
|
<td align="center"><code>false</code></td>
|
|
377
395
|
</tr>
|
|
378
396
|
<tr>
|
|
379
|
-
<td colspan="4">
|
|
397
|
+
<td colspan="4">Deprecated and ignored. Generated files always replace whatever is at their path, so this flag no longer does anything; it is still accepted so existing scripts and config files keep running. Reverting an unwanted replacement is version control's job. Compilation under build still only removes the intermediate sources the run itself generated.</td>
|
|
380
398
|
</tr>
|
|
381
399
|
<tr>
|
|
382
400
|
<td>⚠️ <code>logWarnings</code></td>
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -60,21 +60,36 @@ export type CliConfig = {
|
|
|
60
60
|
* `fast-check`, which consumers must install as a (dev) dependency.
|
|
61
61
|
*/
|
|
62
62
|
readonly examples?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* When true, also emit a `@amritk/api` channel contract per AsyncAPI channel
|
|
65
|
+
* into a `contracts/` subdirectory: one `defineMessages({ ... })` module
|
|
66
|
+
* naming every message that channel carries, keyed by its wire discriminator
|
|
67
|
+
* value, plus a barrel. Requires `input: 'asyncapi'` — the contracts are
|
|
68
|
+
* projected from the document's channels and operations, which no other input
|
|
69
|
+
* format has. Incompatible with `typesOnly`: a contract is a runtime value.
|
|
70
|
+
*
|
|
71
|
+
* The generated modules import `@amritk/api`, which the *consuming* project
|
|
72
|
+
* must have installed; it is not a dependency of the generated parsers.
|
|
73
|
+
*/
|
|
74
|
+
readonly messageContracts?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Property name that says which message a frame is, for the contracts
|
|
77
|
+
* `messageContracts` emits. Defaults to `'type'`, matching `@amritk/api`.
|
|
78
|
+
* A channel that declares its own via `x-mjst: { discriminator }` keeps it —
|
|
79
|
+
* one flag covers a whole run, and a run may span channels that disagree.
|
|
80
|
+
*/
|
|
81
|
+
readonly discriminator?: string;
|
|
63
82
|
/**
|
|
64
83
|
* When true, compile the generated TypeScript files to .js and .d.ts output.
|
|
65
84
|
* The .ts source files are removed after compilation.
|
|
66
85
|
*/
|
|
67
86
|
readonly build?: boolean;
|
|
68
87
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* the root of the output directory, and reclaims those paths freely. With
|
|
75
|
-
* `outFile` the "output directory" is the directory holding that file, so the
|
|
76
|
-
* manifest lands beside it — generated examples and the `build` output already
|
|
77
|
-
* do.
|
|
88
|
+
* @deprecated No longer has any effect, and is accepted only so existing
|
|
89
|
+
* scripts and config files keep running. Generated files always replace
|
|
90
|
+
* whatever is at their path; version control is where an unwanted replacement
|
|
91
|
+
* shows up and gets reverted. `build` still only ever deletes the intermediate
|
|
92
|
+
* sources the run itself committed.
|
|
78
93
|
*/
|
|
79
94
|
readonly force?: boolean;
|
|
80
95
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,10 @@ import { combineGeneratedFiles } from "./combine-files.js";
|
|
|
12
12
|
import { createOutputWriter } from "./create-output-writer.js";
|
|
13
13
|
import { detectHelpersMode } from "./detect-helpers-mode.js";
|
|
14
14
|
import { emitExamples } from "./emit-examples.js";
|
|
15
|
+
import { CONTRACTS_DIR, CONTRACTS_PEER, emitMessageContracts } from "./emit-message-contracts.js";
|
|
15
16
|
import { ensureOutputDir } from "./ensure-output-dir.js";
|
|
16
17
|
import { HELP_TEXT } from "./help-text.js";
|
|
18
|
+
import { isDependencyDeclared } from "./is-dependency-declared.js";
|
|
17
19
|
import { loadAsyncApiDocument } from "./load-asyncapi-document.js";
|
|
18
20
|
import { loadConfig } from "./load-config.js";
|
|
19
21
|
import { loadSchema } from "./load-schema.js";
|
|
@@ -22,6 +24,7 @@ import { parseMetaRequest } from "./parse-meta-request.js";
|
|
|
22
24
|
import { readVersion } from "./read-version.js";
|
|
23
25
|
import { resolveImportExt } from "./resolve-import-ext.js";
|
|
24
26
|
const ROOT_TYPE_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
27
|
+
const DISCRIMINATOR_PATTERN = /^[A-Za-z_$][\w$-]*$/;
|
|
25
28
|
const schemaBaseName = (schemaPath) => basename(schemaPath).replace(/\.[^.]+$/, "");
|
|
26
29
|
const resolveHelpersMode = (config, outputDir) => {
|
|
27
30
|
const helpersMode = config.helpers ?? detectHelpersMode(outputDir);
|
|
@@ -64,7 +67,7 @@ const commitOrDiscard = async (writer, stageAll) => {
|
|
|
64
67
|
return writer.commit();
|
|
65
68
|
};
|
|
66
69
|
const runExamples = async (config, outputDir, tasks) => {
|
|
67
|
-
const writer = await createOutputWriter(outputDir
|
|
70
|
+
const writer = await createOutputWriter(outputDir);
|
|
68
71
|
const written = await commitOrDiscard(writer, async () => {
|
|
69
72
|
for (const task of tasks) {
|
|
70
73
|
await emitExamples({
|
|
@@ -153,7 +156,7 @@ const runSingle = async (config, schemaPath, outputDir) => {
|
|
|
153
156
|
const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
|
|
154
157
|
console.log(`Root type: ${rootTypeName}`);
|
|
155
158
|
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
|
|
156
|
-
const writer = await createOutputWriter(outputDir
|
|
159
|
+
const writer = await createOutputWriter(outputDir);
|
|
157
160
|
const written = await commitOrDiscard(writer, async () => {
|
|
158
161
|
for (const file of files) {
|
|
159
162
|
const isHelper = file.filename.startsWith("_helpers/");
|
|
@@ -183,7 +186,7 @@ const runSingleFile = async (config, schemaPath, outFilePath) => {
|
|
|
183
186
|
console.log(`Root type: ${rootTypeName}`);
|
|
184
187
|
const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? "package", void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
|
|
185
188
|
const combined = combineGeneratedFiles(files);
|
|
186
|
-
const writer = await createOutputWriter(outputDir
|
|
189
|
+
const writer = await createOutputWriter(outputDir);
|
|
187
190
|
const written = await commitOrDiscard(writer, async () => {
|
|
188
191
|
await writer.stage(basename(outFilePath), config.banner ? resolveBanner(config.banner) + combined : combined);
|
|
189
192
|
});
|
|
@@ -211,7 +214,7 @@ const runRecursive = async (config, schemaDir, outputDir) => {
|
|
|
211
214
|
await ensureOutputDir(outputDir);
|
|
212
215
|
const helpersMode = resolveHelpersMode(config, outputDir);
|
|
213
216
|
const sharedHelpers = /* @__PURE__ */ new Map();
|
|
214
|
-
const writer = await createOutputWriter(outputDir
|
|
217
|
+
const writer = await createOutputWriter(outputDir);
|
|
215
218
|
const exampleTasks = [];
|
|
216
219
|
const stageAll = async () => {
|
|
217
220
|
for (const schemaFile of schemaFiles) {
|
|
@@ -255,6 +258,11 @@ ${relPath} \u2192 ${relNoExt}/ (root type: ${rootTypeName})`);
|
|
|
255
258
|
Total files generated: ${writtenTsFiles.length}`);
|
|
256
259
|
}
|
|
257
260
|
};
|
|
261
|
+
const reportContractsPeer = (outputDir) => {
|
|
262
|
+
if (isDependencyDeclared(outputDir, CONTRACTS_PEER))
|
|
263
|
+
return;
|
|
264
|
+
console.log(`Tip: the generated contracts import ${CONTRACTS_PEER}, which is not a declared dependency of the project at ${outputDir}. Install it there (it is a peer of the generated code, not of the mjst CLI).`);
|
|
265
|
+
};
|
|
258
266
|
const runAsyncApi = async (config, documentPath, outputDir) => {
|
|
259
267
|
const document = await loadAsyncApiDocument(config, documentPath);
|
|
260
268
|
const model = extractAsyncApi(document);
|
|
@@ -262,7 +270,8 @@ const runAsyncApi = async (config, documentPath, outputDir) => {
|
|
|
262
270
|
for (const issue of model.issues) {
|
|
263
271
|
console.warn(`Warning: ${issue.path}: ${issue.message}`);
|
|
264
272
|
}
|
|
265
|
-
|
|
273
|
+
const contractsPossible = config.messageContracts === true && model.channels.some((c) => c.messages.length > 0);
|
|
274
|
+
if (schemas.length === 0 && !contractsPossible) {
|
|
266
275
|
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
276
|
process.exit(1);
|
|
268
277
|
}
|
|
@@ -270,8 +279,11 @@ const runAsyncApi = async (config, documentPath, outputDir) => {
|
|
|
270
279
|
await ensureOutputDir(outputDir);
|
|
271
280
|
const helpersMode = resolveHelpersMode(config, outputDir);
|
|
272
281
|
const sharedHelpers = /* @__PURE__ */ new Map();
|
|
273
|
-
const writer = await createOutputWriter(outputDir
|
|
282
|
+
const writer = await createOutputWriter(outputDir);
|
|
274
283
|
const exampleTasks = [];
|
|
284
|
+
const contractIssues = [];
|
|
285
|
+
let contractMessageCount = 0;
|
|
286
|
+
let contractChannelCount = 0;
|
|
275
287
|
const stageAll = async () => {
|
|
276
288
|
for (const extracted of schemas) {
|
|
277
289
|
const { subDir, rootTypeName, schema } = extracted;
|
|
@@ -297,15 +309,40 @@ ${subDir}/ (root type: ${rootTypeName})`);
|
|
|
297
309
|
for (const [filename, content] of sharedHelpers) {
|
|
298
310
|
await writer.stage(filename, content);
|
|
299
311
|
}
|
|
312
|
+
if (config.messageContracts) {
|
|
313
|
+
const contracts = await emitMessageContracts({
|
|
314
|
+
model,
|
|
315
|
+
writer,
|
|
316
|
+
bannerPrefix: config.banner ? resolveBanner(config.banner) : "",
|
|
317
|
+
...config.discriminator !== void 0 ? { discriminator: config.discriminator } : {}
|
|
318
|
+
});
|
|
319
|
+
contractIssues.push(...contracts.issues);
|
|
320
|
+
contractMessageCount = contracts.messageCount;
|
|
321
|
+
contractChannelCount = contracts.channelCount;
|
|
322
|
+
}
|
|
300
323
|
};
|
|
301
324
|
const writtenTsFiles = await commitOrDiscard(writer, stageAll);
|
|
302
325
|
for (const filename of writtenTsFiles)
|
|
303
326
|
console.log(`Generated: ${filename}`);
|
|
327
|
+
if (config.messageContracts) {
|
|
328
|
+
for (const issue of contractIssues) {
|
|
329
|
+
console.warn(`Warning: ${issue.path}: ${issue.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (writtenTsFiles.length === 0) {
|
|
333
|
+
console.error(`Error: nothing was generated from ${documentPath}. No message declares a payload the generators can use, and no channel projected onto a contract (see the warnings above).`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
if (config.messageContracts) {
|
|
337
|
+
console.log(`
|
|
338
|
+
Message contracts: ${contractMessageCount} message(s) across ${contractChannelCount} channel(s)`);
|
|
339
|
+
reportContractsPeer(outputDir);
|
|
340
|
+
}
|
|
304
341
|
if (exampleTasks.length > 0) {
|
|
305
342
|
await runExamples(config, outputDir, exampleTasks);
|
|
306
343
|
}
|
|
307
344
|
if (config.build) {
|
|
308
|
-
await buildOutput(outputDir, writtenTsFiles, config.typesOnly);
|
|
345
|
+
await buildOutput(outputDir, writtenTsFiles.filter((file) => !file.startsWith(`${CONTRACTS_DIR}/`)), config.typesOnly);
|
|
309
346
|
} else {
|
|
310
347
|
console.log(`
|
|
311
348
|
Total files generated: ${writtenTsFiles.length}`);
|
|
@@ -348,6 +385,9 @@ const run = async () => {
|
|
|
348
385
|
console.error("Error: provide only one of --out-dir or --out-file, not both.");
|
|
349
386
|
process.exit(1);
|
|
350
387
|
}
|
|
388
|
+
if (config.force) {
|
|
389
|
+
console.warn("Warning: --force is deprecated and does nothing; generated files always replace what is at their path.");
|
|
390
|
+
}
|
|
351
391
|
if (config.rootType !== void 0 && !ROOT_TYPE_PATTERN.test(config.rootType)) {
|
|
352
392
|
console.error(`Error: invalid --root-type "${config.rootType}". Expected a TypeScript identifier (letters, digits, _ or $, not starting with a digit).`);
|
|
353
393
|
process.exit(1);
|
|
@@ -385,6 +425,23 @@ const run = async () => {
|
|
|
385
425
|
console.error("Error: --export applies to module inputs (typebox, zod, ...); an AsyncAPI document has no exports.");
|
|
386
426
|
process.exit(1);
|
|
387
427
|
}
|
|
428
|
+
} else if (config.messageContracts) {
|
|
429
|
+
console.error("Error: --message-contracts requires --input asyncapi; there are no channels to project otherwise.");
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
if (config.messageContracts && config.typesOnly) {
|
|
433
|
+
console.error("Error: --message-contracts cannot be combined with --types-only, which emits no runtime code.");
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
if (config.discriminator !== void 0) {
|
|
437
|
+
if (!config.messageContracts) {
|
|
438
|
+
console.error("Error: --discriminator only applies to --message-contracts. Add --message-contracts, or drop it.");
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
if (!DISCRIMINATOR_PATTERN.test(config.discriminator)) {
|
|
442
|
+
console.error(`Error: invalid --discriminator "${config.discriminator}". Expected a property name of letters, digits, _, $ or -, not starting with a digit.`);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
388
445
|
}
|
|
389
446
|
if (config.outFile) {
|
|
390
447
|
if (!config.typesOnly) {
|
|
@@ -3,22 +3,21 @@
|
|
|
3
3
|
* final location under a temporary name, and only once the whole set is on disk
|
|
4
4
|
* are the temporaries renamed into place.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* The failure mode it exists for is **partial trees**. Writing straight to the
|
|
7
|
+
* final paths meant a failure halfway through (a `_helpers` path occupied by a
|
|
8
|
+
* regular file, a full disk) left some generated files behind and others missing
|
|
9
|
+
* — a tree that compiles against a schema that no longer exists. Staging first
|
|
10
|
+
* means the run either lands completely or leaves the destination as it found it.
|
|
11
|
+
* Renaming is not one atomic operation across the whole set, but every rename
|
|
12
|
+
* happens after all the risky work (generating, creating directories, writing
|
|
13
|
+
* bytes) has already succeeded, so it is the cheapest place to get the guarantee.
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* all the risky work (generating, creating directories, writing bytes) has
|
|
15
|
-
* already succeeded, so it is the cheapest place to get the guarantee.
|
|
16
|
-
* 2. **Clobbering hand-written files.** A generated name can collide with a file
|
|
17
|
-
* the user wrote (`index.ts` is the common one), which used to be overwritten
|
|
18
|
-
* without a word — and then, under `--build`, deleted along with the other
|
|
19
|
-
* intermediate sources. A file is only replaced when a previous run claimed it
|
|
20
|
-
* (see {@link MANIFEST_FILENAME}) or `--force` says so, and `--build` is only
|
|
21
|
-
* ever handed the paths this run actually committed.
|
|
15
|
+
* A generated path that already exists is simply replaced. The writer keeps no
|
|
16
|
+
* record of what it produced last time: an output directory is generated output,
|
|
17
|
+
* and version control is where a user sees — and reverts — a replacement they did
|
|
18
|
+
* not want. What the writer still guarantees is that `--build` only ever deletes
|
|
19
|
+
* intermediate sources this run committed, so nothing that was already sitting in
|
|
20
|
+
* the output directory is removed.
|
|
22
21
|
*/
|
|
23
22
|
export type OutputWriter = {
|
|
24
23
|
/**
|
|
@@ -34,7 +33,10 @@ export type OutputWriter = {
|
|
|
34
33
|
/**
|
|
35
34
|
* Creates an {@link OutputWriter} rooted at `rootDir`.
|
|
36
35
|
*
|
|
36
|
+
* Still async with nothing to await: it read the previous run's manifest here
|
|
37
|
+
* until that was removed, and every call site already awaits it. Keeping the
|
|
38
|
+
* signature leaves room for setup that does need I/O.
|
|
39
|
+
*
|
|
37
40
|
* @param rootDir - Output root. Every write must resolve inside it.
|
|
38
|
-
* @param force - Overwrite pre-existing files instead of refusing (`--force`).
|
|
39
41
|
*/
|
|
40
|
-
export declare const createOutputWriter: (rootDir: string
|
|
42
|
+
export declare const createOutputWriter: (rootDir: string) => Promise<OutputWriter>;
|
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname,
|
|
1
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve, sep } from "node:path";
|
|
3
3
|
import { ensureOutputDir } from "./ensure-output-dir.js";
|
|
4
|
-
const
|
|
5
|
-
const pathExists = async (path) => stat(path).then(() => true, () => false);
|
|
6
|
-
const readManifest = async (root) => {
|
|
7
|
-
const raw = await readFile(join(root, MANIFEST_FILENAME), "utf-8").catch(() => void 0);
|
|
8
|
-
if (raw === void 0)
|
|
9
|
-
return /* @__PURE__ */ new Set();
|
|
10
|
-
try {
|
|
11
|
-
const parsed = JSON.parse(raw);
|
|
12
|
-
const files = Array.isArray(parsed.files) ? parsed.files : [];
|
|
13
|
-
return new Set(files.filter((file) => typeof file === "string").map((file) => resolve(root, file)));
|
|
14
|
-
} catch {
|
|
15
|
-
return /* @__PURE__ */ new Set();
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
const createOutputWriter = async (rootDir, force = false) => {
|
|
4
|
+
const createOutputWriter = async (rootDir) => {
|
|
19
5
|
const root = resolve(rootDir);
|
|
20
|
-
const owned = await readManifest(root);
|
|
21
6
|
const staged = [];
|
|
22
7
|
const stamp = `${process.pid}-${Date.now().toString(36)}`;
|
|
23
8
|
const stage = async (relativePath, content) => {
|
|
@@ -25,9 +10,6 @@ const createOutputWriter = async (rootDir, force = false) => {
|
|
|
25
10
|
if (targetPath !== root && !targetPath.startsWith(root + sep)) {
|
|
26
11
|
throw new Error(`Refusing to write "${relativePath}": it resolves outside the output directory ${root}.`);
|
|
27
12
|
}
|
|
28
|
-
if (!force && !owned.has(targetPath) && await pathExists(targetPath)) {
|
|
29
|
-
throw new Error(`Refusing to overwrite ${targetPath}: it already exists and was not generated by mjst. Move it aside, choose another output directory, or pass --force to overwrite it.`);
|
|
30
|
-
}
|
|
31
13
|
const tempPath = `${targetPath}.mjst-${stamp}-${staged.length}.tmp`;
|
|
32
14
|
await ensureOutputDir(dirname(targetPath));
|
|
33
15
|
await writeFile(tempPath, content, "utf-8");
|
|
@@ -36,12 +18,6 @@ const createOutputWriter = async (rootDir, force = false) => {
|
|
|
36
18
|
const commit = async () => {
|
|
37
19
|
for (const file of staged) {
|
|
38
20
|
await rename(file.tempPath, file.targetPath);
|
|
39
|
-
owned.add(file.targetPath);
|
|
40
|
-
}
|
|
41
|
-
if (staged.length > 0) {
|
|
42
|
-
const files = [...owned].map((path) => relative(root, path)).sort();
|
|
43
|
-
await writeFile(join(root, MANIFEST_FILENAME), `${JSON.stringify({ version: 1, files }, null, 2)}
|
|
44
|
-
`, "utf-8");
|
|
45
21
|
}
|
|
46
22
|
return staged.map((file) => file.relativePath);
|
|
47
23
|
};
|
|
@@ -3,13 +3,8 @@
|
|
|
3
3
|
* package (`'package'`) or ship the helper sources alongside them (`'embedded'`).
|
|
4
4
|
*
|
|
5
5
|
* We select `'package'` only when `@amritk/helpers` is a *declared* dependency
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `node_modules` purely as a transitive dependency of `@amritk/mjst`, which
|
|
10
|
-
* makes `import '@amritk/helpers/...'` work locally but break under
|
|
11
|
-
* pnpm/isolated installs where undeclared packages are not reachable. Requiring
|
|
12
|
-
* a declaration keeps auto-detected `'package'` output portable across install
|
|
13
|
-
* layouts; everything else falls back to the self-contained `'embedded'` mode.
|
|
6
|
+
* of the target project's nearest `package.json` above `outputDir` — see
|
|
7
|
+
* {@link isDependencyDeclared} for why merely being resolvable is not enough.
|
|
8
|
+
* Everything else falls back to the self-contained `'embedded'` mode.
|
|
14
9
|
*/
|
|
15
10
|
export declare const detectHelpersMode: (outputDir: string) => 'package' | 'embedded';
|
|
@@ -1,32 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const readDeclaredDeps = (packageJsonPath) => {
|
|
4
|
-
try {
|
|
5
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
6
|
-
return /* @__PURE__ */ new Set([
|
|
7
|
-
...Object.keys(pkg.dependencies ?? {}),
|
|
8
|
-
...Object.keys(pkg.devDependencies ?? {}),
|
|
9
|
-
...Object.keys(pkg.peerDependencies ?? {}),
|
|
10
|
-
...Object.keys(pkg.optionalDependencies ?? {})
|
|
11
|
-
]);
|
|
12
|
-
} catch {
|
|
13
|
-
return void 0;
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
const detectHelpersMode = (outputDir) => {
|
|
17
|
-
let dir = resolve(outputDir);
|
|
18
|
-
const { root } = parsePath(dir);
|
|
19
|
-
while (true) {
|
|
20
|
-
const declared = readDeclaredDeps(join(dir, "package.json"));
|
|
21
|
-
if (declared) {
|
|
22
|
-
return declared.has("@amritk/helpers") ? "package" : "embedded";
|
|
23
|
-
}
|
|
24
|
-
if (dir === root)
|
|
25
|
-
break;
|
|
26
|
-
dir = dirname(dir);
|
|
27
|
-
}
|
|
28
|
-
return "embedded";
|
|
29
|
-
};
|
|
1
|
+
import { isDependencyDeclared } from "./is-dependency-declared.js";
|
|
2
|
+
const detectHelpersMode = (outputDir) => isDependencyDeclared(outputDir, "@amritk/helpers") ? "package" : "embedded";
|
|
30
3
|
export {
|
|
31
4
|
detectHelpersMode
|
|
32
5
|
};
|
package/dist/emit-examples.d.ts
CHANGED
|
@@ -11,10 +11,9 @@ export type EmitExamplesOptions = {
|
|
|
11
11
|
/**
|
|
12
12
|
* The run's writer, rooted at the output destination. Examples used to be
|
|
13
13
|
* written straight to disk with `mkdir` + `writeFile`, which meant the one
|
|
14
|
-
* output tree mjst emits that skipped
|
|
15
|
-
*
|
|
16
|
-
* of a directory makes that impossible to reintroduce
|
|
17
|
-
* that does not go through staging, the manifest, and `--force`.
|
|
14
|
+
* output tree mjst emits that skipped staging entirely — a failure part-way
|
|
15
|
+
* through left a half-written `examples/` tree behind. Taking the writer
|
|
16
|
+
* instead of a directory makes that impossible to reintroduce.
|
|
18
17
|
*/
|
|
19
18
|
readonly writer: OutputWriter;
|
|
20
19
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AsyncApiModel, ExtractionIssue } from '@amritk/asyncapi';
|
|
2
|
+
import type { OutputWriter } from './create-output-writer.js';
|
|
3
|
+
/** Subdirectory of the output root the contract modules land in. */
|
|
4
|
+
export declare const CONTRACTS_DIR = "contracts";
|
|
5
|
+
/** The package the generated modules import `defineMessages` from. */
|
|
6
|
+
export declare const CONTRACTS_PEER = "@amritk/api";
|
|
7
|
+
export type EmitMessageContractsOptions = {
|
|
8
|
+
readonly model: AsyncApiModel;
|
|
9
|
+
readonly writer: OutputWriter;
|
|
10
|
+
/** Fallback discriminator for channels that do not declare one (`--discriminator`). */
|
|
11
|
+
readonly discriminator?: string;
|
|
12
|
+
/** Header comment prepended to each file, when `--banner` asked for one. */
|
|
13
|
+
readonly bannerPrefix?: string;
|
|
14
|
+
};
|
|
15
|
+
export type EmitMessageContractsResult = {
|
|
16
|
+
/** Paths staged on the writer, relative to its root, in the order they were staged. */
|
|
17
|
+
readonly files: readonly string[];
|
|
18
|
+
/** Everything a channel could not project, for the caller to warn about. */
|
|
19
|
+
readonly issues: readonly ExtractionIssue[];
|
|
20
|
+
/** How many messages made it into a contract, across every channel. */
|
|
21
|
+
readonly messageCount: number;
|
|
22
|
+
/** How many channels got a contract module (the barrel is not one of them). */
|
|
23
|
+
readonly channelCount: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Projects every channel of an AsyncAPI document onto a `defineMessages`
|
|
27
|
+
* contract and stages one module per channel under `contracts/`, plus a barrel.
|
|
28
|
+
*
|
|
29
|
+
* These are *runtime values*, not types — which is why the flag that asks for
|
|
30
|
+
* them refuses `--types-only`, and why the generated files import
|
|
31
|
+
* `@amritk/api`. That import is the consumer's dependency to satisfy: the CLI
|
|
32
|
+
* needs the package to have been written, not to be installed where the output
|
|
33
|
+
* lands, and pinning it as a hard dependency of generated code would drag a
|
|
34
|
+
* server framework into projects that only wanted the schemas.
|
|
35
|
+
*
|
|
36
|
+
* A channel that projects to nothing (every message skipped, or a channel with
|
|
37
|
+
* no messages at all) is not written: an exported contract with no messages
|
|
38
|
+
* accepts no frame in either direction, which is a trap rather than a document.
|
|
39
|
+
* The reasons come back in {@link EmitMessageContractsResult.issues}.
|
|
40
|
+
*
|
|
41
|
+
* The barrel's specifiers are always `.ts`, ignoring `--import-ext`: these
|
|
42
|
+
* modules are the one part of the output `--build` leaves uncompiled (they
|
|
43
|
+
* import the `@amritk/api` peer, and handing tsc an import the output's project
|
|
44
|
+
* has not installed would fail the parsers' compilation too). A `.js` specifier
|
|
45
|
+
* would then name a file this run never wrote.
|
|
46
|
+
*/
|
|
47
|
+
export declare const emitMessageContracts: (options: EmitMessageContractsOptions) => Promise<EmitMessageContractsResult>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { buildChannelContract, sanitizeToken } from "@amritk/asyncapi";
|
|
2
|
+
const CONTRACTS_DIR = "contracts";
|
|
3
|
+
const CONTRACTS_PEER = "@amritk/api";
|
|
4
|
+
const renderDirection = (name, direction, indent) => {
|
|
5
|
+
const entries = Object.entries(direction).map(([messageName, schema]) => {
|
|
6
|
+
const literal = JSON.stringify(schema, null, 2).replaceAll("\n", `
|
|
7
|
+
${indent} `);
|
|
8
|
+
return `${indent} ${JSON.stringify(messageName)}: ${literal} as const,`;
|
|
9
|
+
});
|
|
10
|
+
return `${indent}${name}: {
|
|
11
|
+
${entries.join("\n")}
|
|
12
|
+
${indent}},`;
|
|
13
|
+
};
|
|
14
|
+
const renderChannelModule = (contract, channel) => {
|
|
15
|
+
const directions = [
|
|
16
|
+
Object.keys(contract.clientToServer).length > 0 ? renderDirection("clientToServer", contract.clientToServer, " ") : void 0,
|
|
17
|
+
Object.keys(contract.serverToClient).length > 0 ? renderDirection("serverToClient", contract.serverToClient, " ") : void 0
|
|
18
|
+
].filter((entry) => entry !== void 0);
|
|
19
|
+
const address = channel.address !== void 0 && channel.address !== channel.key ? ` (${channel.address})` : "";
|
|
20
|
+
return [
|
|
21
|
+
"/**",
|
|
22
|
+
` * Message contract for the AsyncAPI channel \`${channel.key}\`${address}.`,
|
|
23
|
+
" *",
|
|
24
|
+
` * Requires \`${CONTRACTS_PEER}\` at runtime \u2014 it is a peer dependency of this`,
|
|
25
|
+
" * generated code, not of the mjst CLI that wrote it. Install it in the project",
|
|
26
|
+
" * that imports this file.",
|
|
27
|
+
" *",
|
|
28
|
+
" * Message keys are the wire discriminator values: a frame",
|
|
29
|
+
` * \`{ "${contract.discriminator}": "<key>", ... }\` selects its schema by that key, and the`,
|
|
30
|
+
" * tag is removed before the payload below is validated \u2014 which is why no",
|
|
31
|
+
" * payload here declares it.",
|
|
32
|
+
" */",
|
|
33
|
+
`import { defineMessages } from '${CONTRACTS_PEER}'`,
|
|
34
|
+
"",
|
|
35
|
+
`export const ${contract.exportName} = defineMessages({`,
|
|
36
|
+
` discriminator: '${contract.discriminator}',`,
|
|
37
|
+
...directions,
|
|
38
|
+
"})",
|
|
39
|
+
""
|
|
40
|
+
].join("\n");
|
|
41
|
+
};
|
|
42
|
+
const emitMessageContracts = async (options) => {
|
|
43
|
+
const { model, writer, bannerPrefix = "" } = options;
|
|
44
|
+
const issues = [];
|
|
45
|
+
const files = [];
|
|
46
|
+
const barrel = [];
|
|
47
|
+
const takenTokens = /* @__PURE__ */ new Set();
|
|
48
|
+
const takenExports = /* @__PURE__ */ new Set();
|
|
49
|
+
let messageCount = 0;
|
|
50
|
+
for (const channel of model.channels) {
|
|
51
|
+
const contract = buildChannelContract(channel, {
|
|
52
|
+
...options.discriminator !== void 0 ? { discriminator: options.discriminator } : {}
|
|
53
|
+
});
|
|
54
|
+
issues.push(...contract.issues);
|
|
55
|
+
const count = Object.keys(contract.clientToServer).length + Object.keys(contract.serverToClient).length;
|
|
56
|
+
if (count === 0) {
|
|
57
|
+
issues.push({
|
|
58
|
+
path: `#/channels/${channel.key}`,
|
|
59
|
+
message: "no message projected onto a contract; no contract module written for this channel"
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
messageCount += count;
|
|
64
|
+
const token = claim(sanitizeToken(channel.key, "channel"), takenTokens, "-");
|
|
65
|
+
const exportName = claim(contract.exportName, takenExports, "");
|
|
66
|
+
if (exportName !== contract.exportName) {
|
|
67
|
+
issues.push({
|
|
68
|
+
path: `#/channels/${channel.key}`,
|
|
69
|
+
message: `contract export name "${contract.exportName}" already claimed; using "${exportName}"`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const filename = `${CONTRACTS_DIR}/${token}.ts`;
|
|
73
|
+
await writer.stage(filename, bannerPrefix + renderChannelModule({ ...contract, exportName }, channel));
|
|
74
|
+
files.push(filename);
|
|
75
|
+
barrel.push(`export { ${exportName} } from './${token}.ts'`);
|
|
76
|
+
}
|
|
77
|
+
if (barrel.length > 0) {
|
|
78
|
+
const filename = `${CONTRACTS_DIR}/index.ts`;
|
|
79
|
+
await writer.stage(filename, `${bannerPrefix + barrel.join("\n")}
|
|
80
|
+
`);
|
|
81
|
+
files.push(filename);
|
|
82
|
+
}
|
|
83
|
+
return { files, issues, messageCount, channelCount: barrel.length };
|
|
84
|
+
};
|
|
85
|
+
const claim = (base, taken, separator) => {
|
|
86
|
+
let candidate = base;
|
|
87
|
+
for (let n = 2; taken.has(candidate); n++) {
|
|
88
|
+
candidate = `${base}${separator}${n}`;
|
|
89
|
+
}
|
|
90
|
+
taken.add(candidate);
|
|
91
|
+
return candidate;
|
|
92
|
+
};
|
|
93
|
+
export {
|
|
94
|
+
CONTRACTS_DIR,
|
|
95
|
+
CONTRACTS_PEER,
|
|
96
|
+
emitMessageContracts
|
|
97
|
+
};
|
package/dist/help-text.d.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* documents these flags (config.schema.json) is not shipped in the published
|
|
5
5
|
* package; the help-text test cross-checks that every flag is listed.
|
|
6
6
|
*/
|
|
7
|
-
export declare const HELP_TEXT = "mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nSubcommands (each with its own flags and --help):\n mjst lint <files> Lint JSON/YAML documents against a ruleset\n mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect, avro,\n or asyncapi (an AsyncAPI 2.x/3.0 document, JSON or YAML)\n --export <name> Which export of the schema module to use when --input names a module format\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --validators Also emit validateX/isX validators into a validators/ subdir\n --examples Also emit fast-check arbitraries + example values under examples/\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force
|
|
7
|
+
export declare const HELP_TEXT = "mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nSubcommands (each with its own flags and --help):\n mjst lint <files> Lint JSON/YAML documents against a ruleset\n mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect, avro,\n or asyncapi (an AsyncAPI 2.x/3.0 document, JSON or YAML)\n --export <name> Which export of the schema module to use when --input names a module format\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --validators Also emit validateX/isX validators into a validators/ subdir\n --examples Also emit fast-check arbitraries + example values under examples/\n --message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)\n --discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force Deprecated; ignored (generated files always overwrite)\n --import-ext <ext> Extension on generated relative imports: ts (default) or js\n --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)\n --root-type <name> Root type name for a single --schema run (default: schema title or filename)\n --type-suffix <s> Suffix appended to every $ref-derived type name\n --banner [text] Prepend a header comment to every generated file\n --readonly Emit deeply readonly type definitions\n\nValidation:\n --strict Throw on type/shape mismatches instead of coercing to defaults\n --strip-unknown Silently drop undeclared input keys at every nesting level\n --case-insensitive Normalize mis-cased enum/const strings to their exact casing\n --log-warnings console.warn on input keys not declared in the schema\n --unknown-keys <s> How closed objects are proven free of undeclared keys:\n count-keys (default, fastest on Bun) or count-enumerable (fastest on Node)\n\nReference resolution:\n --resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)\n --allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)\n --allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)\n --allowed-roots <d> Extra directories a local $ref may resolve into, on top of the schema's own\n (comma-separated; relative to the current directory)\n\nMisc:\n --config <path> JSON config file with the same keys; CLI flags take precedence\n --version, -v Print the CLI version\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
|
package/dist/help-text.js
CHANGED
|
@@ -22,8 +22,10 @@ Output:
|
|
|
22
22
|
--types-only Generate type definitions only, without parser functions
|
|
23
23
|
--validators Also emit validateX/isX validators into a validators/ subdir
|
|
24
24
|
--examples Also emit fast-check arbitraries + example values under examples/
|
|
25
|
+
--message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)
|
|
26
|
+
--discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)
|
|
25
27
|
--build Compile the generated files to .js/.d.ts (implies --import-ext js)
|
|
26
|
-
--force
|
|
28
|
+
--force Deprecated; ignored (generated files always overwrite)
|
|
27
29
|
--import-ext <ext> Extension on generated relative imports: ts (default) or js
|
|
28
30
|
--helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)
|
|
29
31
|
--root-type <name> Root type name for a single --schema run (default: schema title or filename)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `packageName` is a *declared* dependency (dependencies /
|
|
3
|
+
* devDependencies / peer / optional) of the nearest readable `package.json` at
|
|
4
|
+
* or above `fromDir`.
|
|
5
|
+
*
|
|
6
|
+
* Declared, not merely resolvable: under npm/bun's hoisted layouts a package can
|
|
7
|
+
* sit in `node_modules` purely as a transitive dependency of `@amritk/mjst`,
|
|
8
|
+
* which makes importing it work locally but break under pnpm/isolated installs
|
|
9
|
+
* where undeclared packages are not reachable. Generated code has to survive
|
|
10
|
+
* both, so the question worth asking is whether the *project* asked for the
|
|
11
|
+
* package, not whether this machine happens to have it.
|
|
12
|
+
*
|
|
13
|
+
* Only the nearest package.json is consulted. A workspace root above it may well
|
|
14
|
+
* declare the package, but a nested package that does not declare it cannot rely
|
|
15
|
+
* on the hoist — which is exactly the case this is meant to catch.
|
|
16
|
+
*/
|
|
17
|
+
export declare const isDependencyDeclared: (fromDir: string, packageName: string) => boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, parse as parsePath, resolve } from "node:path";
|
|
3
|
+
const readDeclaredDeps = (packageJsonPath) => {
|
|
4
|
+
try {
|
|
5
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
6
|
+
return /* @__PURE__ */ new Set([
|
|
7
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
8
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
9
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
10
|
+
...Object.keys(pkg.optionalDependencies ?? {})
|
|
11
|
+
]);
|
|
12
|
+
} catch {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const isDependencyDeclared = (fromDir, packageName) => {
|
|
17
|
+
let dir = resolve(fromDir);
|
|
18
|
+
const { root } = parsePath(dir);
|
|
19
|
+
while (true) {
|
|
20
|
+
const declared = readDeclaredDeps(join(dir, "package.json"));
|
|
21
|
+
if (declared)
|
|
22
|
+
return declared.has(packageName);
|
|
23
|
+
if (dir === root)
|
|
24
|
+
break;
|
|
25
|
+
dir = dirname(dir);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
export {
|
|
30
|
+
isDependencyDeclared
|
|
31
|
+
};
|
package/dist/load-config.js
CHANGED
|
@@ -30,6 +30,8 @@ const loadConfig = async (configPath) => {
|
|
|
30
30
|
...typeof obj["validators"] === "boolean" && { validators: obj["validators"] },
|
|
31
31
|
...typeof obj["typesOnly"] === "boolean" && { typesOnly: obj["typesOnly"] },
|
|
32
32
|
...typeof obj["examples"] === "boolean" && { examples: obj["examples"] },
|
|
33
|
+
...typeof obj["messageContracts"] === "boolean" && { messageContracts: obj["messageContracts"] },
|
|
34
|
+
...typeof obj["discriminator"] === "string" && { discriminator: obj["discriminator"] },
|
|
33
35
|
...typeof obj["build"] === "boolean" && { build: obj["build"] },
|
|
34
36
|
...typeof obj["force"] === "boolean" && { force: obj["force"] },
|
|
35
37
|
...typeof obj["logWarnings"] === "boolean" && { logWarnings: obj["logWarnings"] },
|
package/dist/parse-cli-args.js
CHANGED
|
@@ -5,6 +5,7 @@ const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
|
|
|
5
5
|
"typesOnly",
|
|
6
6
|
"validators",
|
|
7
7
|
"examples",
|
|
8
|
+
"messageContracts",
|
|
8
9
|
"build",
|
|
9
10
|
"force",
|
|
10
11
|
"logWarnings",
|
|
@@ -22,6 +23,7 @@ const VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
|
22
23
|
"outFile",
|
|
23
24
|
"input",
|
|
24
25
|
"export",
|
|
26
|
+
"discriminator",
|
|
25
27
|
"helpers",
|
|
26
28
|
"typeSuffix",
|
|
27
29
|
"importExt",
|
|
@@ -74,6 +76,9 @@ const assignValue = (config, key, value) => {
|
|
|
74
76
|
case "rootType":
|
|
75
77
|
config.rootType = value;
|
|
76
78
|
return true;
|
|
79
|
+
case "discriminator":
|
|
80
|
+
config.discriminator = value;
|
|
81
|
+
return true;
|
|
77
82
|
case "input": {
|
|
78
83
|
const parsed = parseInputValue(value);
|
|
79
84
|
if (!parsed) {
|
|
@@ -120,6 +125,9 @@ const assignBoolean = (config, key, value) => {
|
|
|
120
125
|
case "examples":
|
|
121
126
|
config.examples = value;
|
|
122
127
|
return true;
|
|
128
|
+
case "messageContracts":
|
|
129
|
+
config.messageContracts = value;
|
|
130
|
+
return true;
|
|
123
131
|
case "build":
|
|
124
132
|
config.build = value;
|
|
125
133
|
return true;
|
package/dist/validate-config.js
CHANGED
|
@@ -10,6 +10,8 @@ const CONFIG_KEYS = {
|
|
|
10
10
|
typesOnly: { type: "boolean" },
|
|
11
11
|
validators: { type: "boolean" },
|
|
12
12
|
examples: { type: "boolean" },
|
|
13
|
+
messageContracts: { type: "boolean" },
|
|
14
|
+
discriminator: { type: "string" },
|
|
13
15
|
build: { type: "boolean" },
|
|
14
16
|
force: { type: "boolean" },
|
|
15
17
|
logWarnings: { type: "boolean" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/mjst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
"./package.json": "./package.json"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@amritk/adapters": "^0.6.
|
|
52
|
+
"@amritk/adapters": "^0.6.1",
|
|
53
53
|
"@amritk/api": "^0.16.3",
|
|
54
|
-
"@amritk/asyncapi": "^0.
|
|
55
|
-
"@amritk/generate-examples": "^0.8.
|
|
56
|
-
"@amritk/generate-parsers": "^0.
|
|
57
|
-
"@amritk/generate-validators": "^0.16.
|
|
58
|
-
"@amritk/helpers": "^0.
|
|
54
|
+
"@amritk/asyncapi": "^0.2.0",
|
|
55
|
+
"@amritk/generate-examples": "^0.8.3",
|
|
56
|
+
"@amritk/generate-parsers": "^0.22.0",
|
|
57
|
+
"@amritk/generate-validators": "^0.16.1",
|
|
58
|
+
"@amritk/helpers": "^0.20.0",
|
|
59
59
|
"@amritk/lint": "^0.5.3",
|
|
60
60
|
"@amritk/resolve-refs": "^0.7.1",
|
|
61
61
|
"@amritk/yaml": "^0.7.2",
|