@amritk/mjst 0.7.14 → 0.7.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,7 +101,6 @@ npx mjst --config ./mjst.config.json
101
101
  <th>Property</th>
102
102
  <th>CLI Flag</th>
103
103
  <th>Type</th>
104
- <th align="center">Required</th>
105
104
  <th align="center">Default</th>
106
105
  </tr>
107
106
  </thead>
@@ -110,151 +109,136 @@ npx mjst --config ./mjst.config.json
110
109
  <td>📄 <code>schema</code></td>
111
110
  <td><code>--schema &lt;path&gt;</code></td>
112
111
  <td><code>string</code></td>
113
- <td align="center">—</td>
114
- <td align="center">—</td>
112
+ <td align="center"></td>
115
113
  </tr>
116
114
  <tr>
117
- <td colspan="5">Path to the schema to process. With the default 'json' input this is a JSON Schema file that is read and parsed. With any other input format it is a JS/TS module that exports a schema, which is loaded and converted to JSON Schema via the matching adapter. Either 'schema' or 'schemaDir' is required.</td>
115
+ <td colspan="4">Path to the schema to process. With the default 'json' input this is a JSON Schema file that is read and parsed. With any other input format it is a JS/TS module that exports a schema, which is loaded and converted to JSON Schema via the matching adapter. Either 'schema' or 'schemaDir' is required.<br><strong>Examples:</strong> <code>"./schema.json"</code></td>
118
116
  </tr>
119
117
  <tr>
120
118
  <td>🗂️ <code>schemaDir</code></td>
121
119
  <td><code>--schema-dir &lt;dir&gt;</code></td>
122
120
  <td><code>string</code></td>
123
- <td align="center">—</td>
124
- <td align="center">—</td>
121
+ <td align="center"></td>
125
122
  </tr>
126
123
  <tr>
127
- <td colspan="5">Path to a directory of JSON Schema files. When set, the CLI walks the directory recursively, generates parsers for every '*.json' schema it finds, and mirrors the directory layout under outDir (each schema lands in its own subdirectory). The runtime helpers are emitted once into a shared outDir/_helpers/ that every nested parser imports from. Mutually exclusive with 'schema'; when both are present 'schemaDir' wins. Only JSON Schema input is supported in this mode.</td>
124
+ <td colspan="4">Path to a directory of JSON Schema files. When set, the CLI walks the directory recursively, generates parsers for every '*.json' schema it finds, and mirrors the directory layout under outDir (each schema lands in its own subdirectory). The runtime helpers are emitted once into a shared outDir/_helpers/ that every nested parser imports from. Mutually exclusive with 'schema'; when both are present 'schemaDir' wins. Only JSON Schema input is supported in this mode.<br><strong>Examples:</strong> <code>"./schemas"</code></td>
128
125
  </tr>
129
126
  <tr>
130
127
  <td>🔌 <code>input</code></td>
131
128
  <td><code>--input &lt;format&gt;</code></td>
132
129
  <td><code>string</code></td>
133
- <td align="center">—</td>
134
130
  <td align="center"><code>"json"</code></td>
135
131
  </tr>
136
132
  <tr>
137
- <td colspan="5">Source format of the schema. 'json' (default) reads a JSON Schema file directly. 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.</td>
133
+ <td colspan="4">Source format of the schema. 'json' (default) reads a JSON Schema file directly. 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></td>
138
134
  </tr>
139
135
  <tr>
140
136
  <td>📦 <code>export</code></td>
141
137
  <td><code>--export &lt;name&gt;</code></td>
142
138
  <td><code>string</code></td>
143
- <td align="center">—</td>
144
- <td align="center">—</td>
139
+ <td align="center"></td>
145
140
  </tr>
146
141
  <tr>
147
- <td colspan="5">Which export of the schema module to use when 'input' is not 'json'. Defaults to the default export, or the sole named export when the module has exactly one.</td>
142
+ <td colspan="4">Which export of the schema module to use when 'input' is not 'json'. Defaults to the default export, or the sole named export when the module has exactly one.</td>
148
143
  </tr>
149
144
  <tr>
150
145
  <td>📁 <code>outDir</code></td>
151
146
  <td><code>--out-dir &lt;dir&gt;</code></td>
152
147
  <td><code>string</code></td>
153
- <td align="center">—</td>
154
- <td align="center">—</td>
148
+ <td align="center"></td>
155
149
  </tr>
156
150
  <tr>
157
- <td colspan="5">Output directory for generated TypeScript files. The directory is created automatically if it does not exist. Subdirectories are created as needed when a generated file includes a nested path. Mutually exclusive with 'outFile'.</td>
151
+ <td colspan="4">Output directory for generated TypeScript files. The directory is created automatically if it does not exist. Subdirectories are created as needed when a generated file includes a nested path. Mutually exclusive with 'outFile'.<br><strong>Examples:</strong> <code>"./generated"</code></td>
158
152
  </tr>
159
153
  <tr>
160
154
  <td>📄 <code>outFile</code></td>
161
155
  <td><code>--out-file &lt;file&gt;</code></td>
162
156
  <td><code>string</code></td>
163
- <td align="center">—</td>
164
- <td align="center">—</td>
157
+ <td align="center"></td>
165
158
  </tr>
166
159
  <tr>
167
- <td colspan="5">Output everything to a single TypeScript file instead of a directory. Every generated definition is concatenated into one self-contained file and the cross-file imports are dropped. Mutually exclusive with 'outDir'. Currently supported only together with 'typesOnly'.</td>
160
+ <td colspan="4">Output everything to a single TypeScript file instead of a directory. Every generated definition is concatenated into one self-contained file and the cross-file imports are dropped. Mutually exclusive with 'outDir'. Currently supported only together with 'typesOnly'.<br><strong>Examples:</strong> <code>"./generated/schema.ts"</code></td>
168
161
  </tr>
169
162
  <tr>
170
163
  <td>🏷️ <code>typesOnly</code></td>
171
164
  <td><code>--types-only</code></td>
172
165
  <td><code>boolean</code></td>
173
- <td align="center">—</td>
174
166
  <td align="center"><code>false</code></td>
175
167
  </tr>
176
168
  <tr>
177
- <td colspan="5">Generate only TypeScript type definitions without parser functions. Useful when you only need the type shapes and do not need runtime validation.</td>
169
+ <td colspan="4">Generate only TypeScript type definitions without parser functions. Useful when you only need the type shapes and do not need runtime validation.</td>
178
170
  </tr>
179
171
  <tr>
180
172
  <td>🔨 <code>build</code></td>
181
173
  <td><code>--build</code></td>
182
174
  <td><code>boolean</code></td>
183
- <td align="center">—</td>
184
175
  <td align="center"><code>false</code></td>
185
176
  </tr>
186
177
  <tr>
187
- <td colspan="5">Compile the generated TypeScript files to .js and .d.ts output. A temporary tsconfig is written to the output directory, tsc is invoked, and the intermediate .ts source files are removed when compilation succeeds.</td>
178
+ <td colspan="4">Compile the generated TypeScript files to .js and .d.ts output. A temporary tsconfig is written to the output directory, tsc is invoked, and the intermediate .ts source files are removed when compilation succeeds.</td>
188
179
  </tr>
189
180
  <tr>
190
181
  <td>⚠️ <code>logWarnings</code></td>
191
182
  <td><code>--log-warnings</code></td>
192
183
  <td><code>boolean</code></td>
193
- <td align="center">—</td>
194
184
  <td align="center"><code>false</code></td>
195
185
  </tr>
196
186
  <tr>
197
- <td colspan="5">Emit a console.warn in the generated parsers for every input key that is not declared in the schema's properties. Useful for detecting schema drift or unexpected data shapes at runtime.</td>
187
+ <td colspan="4">Emit a console.warn in the generated parsers for every input key that is not declared in the schema's properties. Useful for detecting schema drift or unexpected data shapes at runtime.</td>
198
188
  </tr>
199
189
  <tr>
200
190
  <td>🚫 <code>strict</code></td>
201
191
  <td><code>--strict</code></td>
202
192
  <td><code>boolean</code></td>
203
- <td align="center">—</td>
204
193
  <td align="center"><code>false</code></td>
205
194
  </tr>
206
195
  <tr>
207
- <td colspan="5">Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. When a schema sets additionalProperties: false, undeclared keys throw too; otherwise they are still allowed.</td>
196
+ <td colspan="4">Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. When a schema sets additionalProperties: false, undeclared keys throw too; otherwise they are still allowed.</td>
208
197
  </tr>
209
198
  <tr>
210
199
  <td>🧹 <code>stripUnknown</code></td>
211
200
  <td><code>--strip-unknown</code></td>
212
201
  <td><code>boolean</code></td>
213
- <td align="center">—</td>
214
202
  <td align="center"><code>false</code></td>
215
203
  </tr>
216
204
  <tr>
217
- <td colspan="5">Build each parser's result from the schema's declared properties only, silently dropping undeclared input keys at every nesting level (zod's .strip()). Extras are never a validation error, so this composes with strict (which still throws on wrong types and missing required properties) and yields to additionalProperties: false, which rejects rather than strips in strict mode.</td>
205
+ <td colspan="4">Build each parser's result from the schema's declared properties only, silently dropping undeclared input keys at every nesting level (zod's .strip()). Extras are never a validation error, so this composes with strict (which still throws on wrong types and missing required properties) and yields to additionalProperties: false, which rejects rather than strips in strict mode.</td>
218
206
  </tr>
219
207
  <tr>
220
208
  <td>🔒 <code>readonly</code></td>
221
209
  <td><code>--readonly</code></td>
222
210
  <td><code>boolean</code></td>
223
- <td align="center">—</td>
224
211
  <td align="center"><code>false</code></td>
225
212
  </tr>
226
213
  <tr>
227
- <td colspan="5">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>
214
+ <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>
228
215
  </tr>
229
216
  <tr>
230
217
  <td>🧰 <code>helpers</code></td>
231
218
  <td><code>--helpers &lt;mode&gt;</code></td>
232
219
  <td><code>string</code></td>
233
- <td align="center">—</td>
234
- <td align="center">—</td>
220
+ <td align="center"></td>
235
221
  </tr>
236
222
  <tr>
237
- <td colspan="5">Controls how generated parsers reference their runtime helpers. 'package' emits imports from @amritk/helpers (requires it to be installed in the consumer project). 'embedded' ships the helper source under outDir/_helpers/ so the output is self-contained. When omitted, the CLI auto-detects: it picks 'package' if @amritk/helpers resolves from outDir, otherwise 'embedded'.</td>
223
+ <td colspan="4">Controls how generated parsers reference their runtime helpers. 'package' emits imports from @amritk/helpers (requires it to be installed in the consumer project). 'embedded' ships the helper source under outDir/_helpers/ so the output is self-contained. When omitted, the CLI auto-detects: it picks 'package' if @amritk/helpers resolves from outDir, otherwise 'embedded'.<br><strong>Allowed:</strong> <code>"package"</code>, <code>"embedded"</code></td>
238
224
  </tr>
239
225
  <tr>
240
226
  <td>🏷️ <code>typeSuffix</code></td>
241
227
  <td><code>--type-suffix &lt;suffix&gt;</code></td>
242
228
  <td><code>string</code></td>
243
- <td align="center">—</td>
244
229
  <td align="center"><code>""</code></td>
245
230
  </tr>
246
231
  <tr>
247
- <td colspan="5">Suffix appended to every generated type name derived from a $ref (e.g. 'Object' turns Contact into ContactObject). Defaults to no suffix. The root type name is used verbatim and is unaffected.</td>
232
+ <td colspan="4">Suffix appended to every generated type name derived from a $ref (e.g. 'Object' turns Contact into ContactObject). Defaults to no suffix. The root type name is used verbatim and is unaffected.</td>
248
233
  </tr>
249
234
  <tr>
250
235
  <td>⚙️ <code>config</code></td>
251
236
  <td><code>--config &lt;path&gt;</code></td>
252
237
  <td><code>string</code></td>
253
- <td align="center">—</td>
254
- <td align="center">—</td>
238
+ <td align="center"></td>
255
239
  </tr>
256
240
  <tr>
257
- <td colspan="5">Path to a JSON config file. Keys match the option names in this schema (schema, schemaDir, outDir, outFile, typesOnly, build, logWarnings, strict, readonly, helpers, typeSuffix). CLI flags take precedence over config file values.</td>
241
+ <td colspan="4">Path to a JSON config file. Keys match the option names in this schema (schema, schemaDir, outDir, outFile, typesOnly, build, logWarnings, strict, readonly, helpers, typeSuffix). CLI flags take precedence over config file values.<br><strong>Examples:</strong> <code>"./mjst.config.json"</code></td>
258
242
  </tr>
259
243
  </tbody>
260
244
  </table>
package/dist/cli.js CHANGED
@@ -80,9 +80,16 @@ const buildOutput = async (outputDir, tsFiles, typesOnly) => {
80
80
  await execFileAsync('npx', ['tsc', '--project', tsconfigPath]);
81
81
  }
82
82
  catch (error) {
83
- // tsc reports the actual errors on stdout; surface them so the user sees
84
- // *what* failed instead of a bare "compilation failed".
85
83
  const details = error;
84
+ // A spawn failure (ENOENT) means the *tool* is missing, not that the code is
85
+ // wrong — pointing the user at "the generated files" would send them hunting
86
+ // for a compile error that doesn't exist. Report the missing executable.
87
+ if (details.code === 'ENOENT') {
88
+ throw new Error('Could not run `npx tsc` to compile the generated files: `npx` (or `tsc`) was not found. ' +
89
+ 'Install TypeScript (e.g. `npm install -D typescript`) or drop --build to skip compilation.');
90
+ }
91
+ // Otherwise tsc ran and reported real errors on stdout; surface them so the
92
+ // user sees *what* failed instead of a bare "compilation failed".
86
93
  const output = [details.stdout, details.stderr].filter(Boolean).join('\n').trim();
87
94
  throw new Error(`TypeScript compilation failed. Check the generated files for errors.${output ? `\n\n${output}` : ''}`);
88
95
  }
@@ -104,6 +111,11 @@ const findJsonSchemas = async (dir) => {
104
111
  for (const entry of entries) {
105
112
  const full = join(dir, entry.name);
106
113
  if (entry.isDirectory()) {
114
+ // `node_modules` holds dependency schemas that aren't the user's, and
115
+ // dot-directories (`.git`, `.cache`, ...) hold tooling state — descending
116
+ // into either pulls in `.json` files the user never meant to generate from.
117
+ if (entry.name === 'node_modules' || entry.name.startsWith('.'))
118
+ continue;
107
119
  results.push(...(await findJsonSchemas(full)));
108
120
  }
109
121
  else if (entry.isFile() && entry.name.endsWith('.json')) {
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
  const SOURCE_FORMATS = ['json', 'typebox', 'zod', 'valibot', 'effect'];
4
4
  const isSourceFormat = (value) => typeof value === 'string' && SOURCE_FORMATS.includes(value);
5
+ const isHelpersMode = (value) => value === 'package' || value === 'embedded';
5
6
  /**
6
7
  * Loads a JSON config file and returns the relevant CLI config properties.
7
8
  * The config file should have the same keys as the CLI flags (schema, outDir).
@@ -27,5 +28,9 @@ export const loadConfig = async (configPath) => {
27
28
  ...(typeof obj['strict'] === 'boolean' && { strict: obj['strict'] }),
28
29
  ...(typeof obj['stripUnknown'] === 'boolean' && { stripUnknown: obj['stripUnknown'] }),
29
30
  ...(typeof obj['readonly'] === 'boolean' && { readonly: obj['readonly'] }),
31
+ ...(isHelpersMode(obj['helpers']) && { helpers: obj['helpers'] }),
32
+ ...(typeof obj['typeSuffix'] === 'string' && { typeSuffix: obj['typeSuffix'] }),
33
+ // `banner` is a boolean toggle or a custom header string, mirroring the CLI flag.
34
+ ...((typeof obj['banner'] === 'boolean' || typeof obj['banner'] === 'string') && { banner: obj['banner'] }),
30
35
  };
31
36
  };
@@ -19,6 +19,11 @@ const VALUE_KEYS = new Set([
19
19
  'helpers',
20
20
  'typeSuffix',
21
21
  ]);
22
+ // Recognized flags that don't map into CliConfig because they're consumed
23
+ // earlier in the pipeline (e.g. `--config` is read by `extractConfigPath` to
24
+ // load the config file before flags are overlaid). They still take a value, so
25
+ // list them here to keep the unknown-flag guard from rejecting them.
26
+ const EXTERNAL_VALUE_KEYS = new Set(['config']);
22
27
  /** Normalizes a CLI flag name to its camelCase config key so both `--out-dir` and `--outDir` map to `outDir`. */
23
28
  const toCamelCase = (key) => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
24
29
  const parseHelpersValue = (value) => {
@@ -119,12 +124,19 @@ export const parseCliArgs = (args) => {
119
124
  else if (BOOLEAN_KEYS.has(key)) {
120
125
  assignBoolean(config, key, value !== 'false');
121
126
  }
122
- else {
127
+ else if (VALUE_KEYS.has(key)) {
123
128
  assignValue(config, key, value);
124
129
  }
130
+ else if (!EXTERNAL_VALUE_KEYS.has(key)) {
131
+ // An unrecognized flag is almost always a typo (e.g. `--strcit`). Silently
132
+ // dropping it means the user gets non-strict output while believing they
133
+ // asked for strict — fail loudly instead of guessing intent.
134
+ throw new Error(`Unknown flag "--${arg.slice(2, equalsIndex)}".`);
135
+ }
125
136
  continue;
126
137
  }
127
138
  const key = toCamelCase(arg.slice(2));
139
+ const flagName = arg.slice(2);
128
140
  // --banner: presence alone enables the default message; an immediately
129
141
  // following non-flag argument is treated as a custom message string.
130
142
  if (key === 'banner') {
@@ -143,14 +155,26 @@ export const parseCliArgs = (args) => {
143
155
  assignBoolean(config, key, true);
144
156
  continue;
145
157
  }
146
- // Value flag: consume the next argument when it is not another flag
158
+ // Value flag: consume the next argument. It must be present and must not be
159
+ // another flag — `--schema --out-dir dist` means `--schema` lost its value,
160
+ // which would otherwise silently fall back to a default.
147
161
  if (VALUE_KEYS.has(key)) {
148
162
  const value = args[i + 1];
149
- if (value && !value.startsWith('--')) {
150
- assignValue(config, key, value);
151
- i++;
163
+ if (value === undefined || value.startsWith('--')) {
164
+ throw new Error(`Flag "--${flagName}" expects a value.`);
152
165
  }
166
+ assignValue(config, key, value);
167
+ i++;
168
+ continue;
169
+ }
170
+ // A flag consumed elsewhere (e.g. `--config`) still swallows its value here.
171
+ if (EXTERNAL_VALUE_KEYS.has(key)) {
172
+ const value = args[i + 1];
173
+ if (value !== undefined && !value.startsWith('--'))
174
+ i++;
175
+ continue;
153
176
  }
177
+ throw new Error(`Unknown flag "--${flagName}".`);
154
178
  }
155
179
  return config;
156
180
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/mjst",
3
- "version": "0.7.14",
3
+ "version": "0.7.16",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,10 +41,10 @@
41
41
  "generate-readme": "bun run ./scripts/generate-readme.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@amritk/adapters": "0.2.12",
45
- "@amritk/generate-parsers": "0.12.2",
46
- "@amritk/generate-markdown": "0.4.0",
47
- "@amritk/helpers": "0.10.1"
44
+ "@amritk/adapters": "0.2.14",
45
+ "@amritk/generate-parsers": "0.13.0",
46
+ "@amritk/generate-markdown": "0.4.1",
47
+ "@amritk/helpers": "0.10.3"
48
48
  },
49
49
  "devDependencies": {
50
50
  "json-schema-typed": "^8.0.1"