@amritk/mjst 0.7.16 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  ![license](https://img.shields.io/badge/license-MIT-22c55e?style=flat-square) 
10
10
  ![JSON Schema](https://img.shields.io/badge/JSON%20Schema-2020--12-f97316?style=flat-square) 
11
11
  ![node](https://img.shields.io/badge/node-%E2%89%A520-339933?style=flat-square&logo=node.js&logoColor=white) 
12
- ![vibe coded](https://img.shields.io/badge/vibe%20coded-24%25-a855f7?style=flat-square)
12
+ ![vibe coded](https://img.shields.io/badge/vibe-coded-a855f7?style=flat-square)
13
13
 
14
14
  </div>
15
15
 
@@ -71,15 +71,39 @@ npx mjst --schema-dir ./schemas --out-dir ./generated
71
71
  ```
72
72
  schemas/ generated/
73
73
  user.json ─▶ user/
74
- api/ document.ts, index.ts
74
+ api/ user.ts, index.ts
75
75
  order.json ─▶ api/order/
76
- document.ts, index.ts
76
+ order.ts, index.ts
77
77
  _helpers/ ← shared runtime helpers (embedded mode)
78
78
  ```
79
79
 
80
80
  Each schema lands in its own subdirectory so generated files never collide, and in embedded
81
81
  mode the runtime helpers are emitted **once** into `outDir/_helpers/` — every nested parser
82
- imports from that single shared location. `--build` compiles the whole tree in place.
82
+ imports from that single shared location. `--build` compiles the whole tree in place. Each
83
+ schema's root type is named after the schema (its `title`, else its filename in PascalCase),
84
+ so `user.json` yields `User` / `parseUser` and `api/order.json` yields `Order` / `parseOrder`.
85
+
86
+ ### Running the generated output
87
+
88
+ By default mjst emits relative imports with the literal `.ts` extension (e.g.
89
+ `import { parseUser } from './user.ts'`), so the generated sources run **as-is** — no build
90
+ step — under:
91
+
92
+ - **Bun** (`bun run generated/index.ts`)
93
+ - **Node ≥ 22.6** type stripping (`node generated/index.ts`; unflagged from 22.18/23, else pass
94
+ `--experimental-strip-types`)
95
+ - **tsc**, provided your `tsconfig.json` sets `"allowImportingTsExtensions": true`
96
+
97
+ If you'd rather compile the output first, pass `--build` (mjst switches the imports to the
98
+ NodeNext `./user.js` form and runs `tsc` for you), or generate with `--import-ext js` to emit
99
+ the compile-ready form without building. `--import-ext ts` together with `--build` is rejected,
100
+ since tsc cannot emit from `.ts` specifiers.
101
+
102
+ > [!NOTE]
103
+ > **Breaking change (0.9.0):** the root type is no longer always named `Document`. It is derived
104
+ > from the schema `title`, falling back to the schema filename (`spec-plan.json` → `SpecPlan`,
105
+ > giving `parseSpecPlan` / `validateSpecPlanShape`). Pass `--root-type <Name>` to override it for
106
+ > a single `--schema` run. Update any imports that referenced `Document` / `parseDocument`.
83
107
 
84
108
  ### Config file
85
109
 
@@ -220,7 +244,7 @@ npx mjst --config ./mjst.config.json
220
244
  <td align="center"></td>
221
245
  </tr>
222
246
  <tr>
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>
247
+ <td colspan="4">Controls how generated parsers reference their runtime helpers. 'package' emits imports from @amritk/helpers (requires it to be installed in the consumer project). 'embedded' ships the helper source under outDir/_helpers/ so the output is self-contained. When omitted, the CLI auto-detects: it picks 'package' only when @amritk/helpers is a declared dependency (dependencies/devDependencies) of the nearest package.json above outDir, otherwise 'embedded'. Requiring a declaration (rather than mere resolvability) keeps auto-detected 'package' output portable across pnpm/isolated installs, where an undeclared transitive @amritk/helpers is not reachable at runtime.<br><strong>Allowed:</strong> <code>"package"</code>, <code>"embedded"</code></td>
224
248
  </tr>
225
249
  <tr>
226
250
  <td>🏷️ <code>typeSuffix</code></td>
@@ -232,13 +256,31 @@ npx mjst --config ./mjst.config.json
232
256
  <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>
233
257
  </tr>
234
258
  <tr>
259
+ <td>🔗 <code>importExt</code></td>
260
+ <td><code>--import-ext &lt;ext&gt;</code></td>
261
+ <td><code>string</code></td>
262
+ <td align="center"><code>"ts"</code></td>
263
+ </tr>
264
+ <tr>
265
+ <td colspan="4">Extension emitted on every relative import specifier in the generated output (cross-file $ref imports, the index barrel, and embedded-helper imports). 'ts' (default) emits the literal on-disk paths so the generated .ts sources load directly under Bun, Node's type stripping (Node 22.6+ with --experimental-strip-types, unflagged from 22.18/23), and tsc when the consumer sets allowImportingTsExtensions — no build step required. 'js' is the standard TS NodeNext form ('./x.js' resolving to a sibling x.ts) for output you compile; it is selected automatically under 'build' since tsc refuses to emit from .ts specifiers. Passing --import-ext ts together with --build is an error.<br><strong>Allowed:</strong> <code>"js"</code>, <code>"ts"</code></td>
266
+ </tr>
267
+ <tr>
268
+ <td>🌱 <code>rootType</code></td>
269
+ <td><code>--root-type &lt;name&gt;</code></td>
270
+ <td><code>string</code></td>
271
+ <td align="center"></td>
272
+ </tr>
273
+ <tr>
274
+ <td colspan="4">Name for the root type of a single --schema run (e.g. 'Program' yields parseProgram / validateProgramShape). When omitted, the name is derived from the schema's title, falling back to the schema filename in PascalCase (spec-plan.json → SpecPlan) and then to 'Document'. Not supported with --schema-dir, where each schema derives its own root type from its filename.<br><strong>Examples:</strong> <code>"Program"</code></td>
275
+ </tr>
276
+ <tr>
235
277
  <td>⚙️ <code>config</code></td>
236
278
  <td><code>--config &lt;path&gt;</code></td>
237
279
  <td><code>string</code></td>
238
280
  <td align="center"></td>
239
281
  </tr>
240
282
  <tr>
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>
283
+ <td colspan="4">Path to a JSON config file. Keys match the option names in this schema (schema, schemaDir, outDir, outFile, typesOnly, build, logWarnings, strict, readonly, helpers, typeSuffix, importExt, rootType). CLI flags take precedence over config file values.<br><strong>Examples:</strong> <code>"./mjst.config.json"</code></td>
242
284
  </tr>
243
285
  </tbody>
244
286
  </table>
@@ -89,4 +89,26 @@ export type CliConfig = {
89
89
  * - `false` / omitted — no header.
90
90
  */
91
91
  readonly banner?: boolean | string;
92
+ /**
93
+ * Extension emitted on every relative import specifier in the generated
94
+ * output (cross-file `$ref` imports, the index barrel, and embedded-helper
95
+ * imports).
96
+ * - `'ts'` (default): the literal on-disk paths, so the generated `.ts`
97
+ * sources load under Bun, Node's type stripping (Node 22.6+ with
98
+ * `--experimental-strip-types`, unflagged from 22.18/23), and tsc with
99
+ * `allowImportingTsExtensions`. Incompatible with `build` — tsc refuses
100
+ * to emit from `.ts` specifiers.
101
+ * - `'js'`: the standard TS NodeNext form (`./x.js` resolving to a sibling
102
+ * `x.ts`), for output that will be compiled. Used automatically when
103
+ * `build` is set and no extension was chosen explicitly.
104
+ */
105
+ readonly importExt?: 'js' | 'ts';
106
+ /**
107
+ * Name for the root type of a single-`schema` run (e.g. `'Program'` yields
108
+ * `parseProgram` / `validateProgramShape`). When omitted, the name is
109
+ * derived from the schema's `title`, falling back to the schema filename in
110
+ * PascalCase (`spec-plan.json` → `SpecPlan`) and then to `'Document'`.
111
+ * Not supported with `schemaDir`, where each schema names its own root.
112
+ */
113
+ readonly rootType?: string;
92
114
  };
package/dist/cli.js CHANGED
@@ -9,12 +9,33 @@ import { deriveRootTypeName } from '@amritk/helpers/derive-root-type-name';
9
9
  const execFileAsync = promisify(execFile);
10
10
  import { combineGeneratedFiles } from './combine-files.js';
11
11
  import { detectHelpersMode } from './detect-helpers-mode.js';
12
+ import { HELP_TEXT } from './help-text.js';
12
13
  import { loadConfig } from './load-config.js';
13
14
  import { loadSchemaModule } from './load-schema-module.js';
14
15
  import { parseCliArgs } from './parse-cli-args.js';
15
16
  import { readVersion } from './read-version.js';
17
+ import { resolveImportExt } from './resolve-import-ext.js';
16
18
  /** True when the args request the CLI version (`version`, `--version`, or `-v`). */
17
19
  const isVersionRequest = (args) => args[0] === 'version' || args.includes('--version') || args.includes('-v');
20
+ /** True when the args request usage help (`help`, `--help`, or `-h`). */
21
+ const isHelpRequest = (args) => args[0] === 'help' || args.includes('--help') || args.includes('-h');
22
+ /** The schema's base filename without extension, used to derive the root type name. */
23
+ const schemaBaseName = (schemaPath) => basename(schemaPath).replace(/\.[^.]+$/, '');
24
+ /**
25
+ * Resolves and reports the helpers mode. An explicit `--helpers` wins; otherwise
26
+ * it is auto-detected from the nearest package.json. When detection falls back to
27
+ * embedded, nudge the user toward declaring `@amritk/helpers` so multiple
28
+ * generated trees can share a single runtime helper copy.
29
+ */
30
+ const resolveHelpersMode = (config, outputDir) => {
31
+ const helpersMode = config.helpers ?? detectHelpersMode(outputDir);
32
+ console.log(`Helpers mode: ${helpersMode}${config.helpers ? ' (explicit)' : ' (auto-detected)'}`);
33
+ if (!config.helpers && helpersMode === 'embedded') {
34
+ console.log('Tip: add @amritk/helpers as a dependency to share one runtime helper copy across generated trees ' +
35
+ '(or pass --helpers embedded to silence this).');
36
+ }
37
+ return helpersMode;
38
+ };
18
39
  /**
19
40
  * Extracts the --config flag value from process args before full parsing.
20
41
  * We need this early so we can load the config file first, then overlay CLI flags.
@@ -128,11 +149,10 @@ const findJsonSchemas = async (dir) => {
128
149
  const runSingle = async (config, schemaPath, outputDir) => {
129
150
  const schema = await loadSchema(config, schemaPath);
130
151
  await mkdir(outputDir, { recursive: true });
131
- const helpersMode = config.helpers ?? detectHelpersMode(outputDir);
132
- console.log(`Helpers mode: ${helpersMode}${config.helpers ? ' (explicit)' : ' (auto-detected)'}`);
133
- const rootTypeName = deriveRootTypeName(schema);
152
+ const helpersMode = resolveHelpersMode(config, outputDir);
153
+ const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
134
154
  console.log(`Root type: ${rootTypeName}`);
135
- const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, undefined, config.readonly, config.stripUnknown, config.typeSuffix);
155
+ const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, undefined, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config));
136
156
  for (const file of files) {
137
157
  const isHelper = file.filename.startsWith('_helpers/');
138
158
  const content = config.banner && !isHelper ? resolveBanner(config.banner) + file.content : file.content;
@@ -155,9 +175,9 @@ const runSingleFile = async (config, schemaPath, outFilePath) => {
155
175
  const schema = await loadSchema(config, schemaPath);
156
176
  const outputDir = dirname(outFilePath);
157
177
  await mkdir(outputDir, { recursive: true });
158
- const rootTypeName = deriveRootTypeName(schema);
178
+ const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
159
179
  console.log(`Root type: ${rootTypeName}`);
160
- const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? 'package', undefined, config.readonly, config.stripUnknown, config.typeSuffix);
180
+ const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? 'package', undefined, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config));
161
181
  const combined = combineGeneratedFiles(files);
162
182
  await writeFile(outFilePath, config.banner ? resolveBanner(config.banner) + combined : combined, 'utf-8');
163
183
  console.log(`Generated: ${relative(process.cwd(), outFilePath)}`);
@@ -185,8 +205,7 @@ const runRecursive = async (config, schemaDir, outputDir) => {
185
205
  process.exit(1);
186
206
  }
187
207
  await mkdir(outputDir, { recursive: true });
188
- const helpersMode = config.helpers ?? detectHelpersMode(outputDir);
189
- console.log(`Helpers mode: ${helpersMode}${config.helpers ? ' (explicit)' : ' (auto-detected)'}`);
208
+ const helpersMode = resolveHelpersMode(config, outputDir);
190
209
  // Helper sources are identical across schemas, so collect them by filename and
191
210
  // write the deduplicated set once at the output root.
192
211
  const sharedHelpers = new Map();
@@ -200,9 +219,9 @@ const runRecursive = async (config, schemaDir, outputDir) => {
200
219
  const depth = relNoExt.split(sep).filter(Boolean).length;
201
220
  const helpersImportPrefix = '../'.repeat(depth);
202
221
  const schema = await loadSchema(config, schemaFile);
203
- const rootTypeName = deriveRootTypeName(schema);
222
+ const rootTypeName = deriveRootTypeName(schema, schemaBaseName(schemaFile));
204
223
  console.log(`\n${relPath} → ${relNoExt}/ (root type: ${rootTypeName})`);
205
- const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix);
224
+ const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config));
206
225
  for (const file of files) {
207
226
  if (file.filename.startsWith('_helpers/')) {
208
227
  sharedHelpers.set(file.filename, file.content);
@@ -234,6 +253,12 @@ const run = async () => {
234
253
  console.log(await readVersion());
235
254
  return;
236
255
  }
256
+ // Bare `mjst` and `mjst --help` both print usage rather than erroring on a
257
+ // missing --out-dir — a first-run user needs to see the flags, not a failure.
258
+ if (args.length === 0 || isHelpRequest(args)) {
259
+ console.log(HELP_TEXT);
260
+ return;
261
+ }
237
262
  const configPath = extractConfigPath(args);
238
263
  // Start with config file values if provided, then overlay CLI flags on top
239
264
  const fileConfig = configPath ? await loadConfig(configPath) : {};
@@ -243,6 +268,19 @@ const run = async () => {
243
268
  console.error('Error: provide only one of --out-dir or --out-file, not both.');
244
269
  process.exit(1);
245
270
  }
271
+ // --root-type names a single root; under --schema-dir each schema derives its
272
+ // own root name, so a single override would be ambiguous.
273
+ if (config.rootType && config.schemaDir) {
274
+ console.error('Error: --root-type cannot be combined with --schema-dir; each schema derives its own root type.');
275
+ process.exit(1);
276
+ }
277
+ // tsc cannot emit from `.ts` import specifiers (allowImportingTsExtensions
278
+ // requires noEmit), so compiled output and stripping-ready sources are
279
+ // mutually exclusive.
280
+ if (config.build && config.importExt === 'ts') {
281
+ console.error('Error: --import-ext ts cannot be combined with --build. Drop --build to keep runnable .ts sources.');
282
+ process.exit(1);
283
+ }
246
284
  // Single-file output is the alternative to a directory of files.
247
285
  if (config.outFile) {
248
286
  if (!config.typesOnly) {
@@ -1,10 +1,15 @@
1
1
  /**
2
- * Detects whether `@amritk/helpers` is resolvable from the consumer's output
3
- * directory using standard Node module resolution (walks up `node_modules`).
2
+ * Detects whether generated parsers should import `@amritk/helpers` from the
3
+ * package (`'package'`) or ship the helper sources alongside them (`'embedded'`).
4
4
  *
5
- * Returns `'package'` when generated parsers can safely
6
- * `import { ... } from '@amritk/helpers/...'` at runtime, or `'embedded'`
7
- * when the helper sources need to be shipped alongside the generated files
8
- * for the output to be self-contained.
5
+ * We select `'package'` only when `@amritk/helpers` is a *declared* dependency
6
+ * (dependencies / devDependencies / peer / optional) of the target project's
7
+ * nearest `package.json` above `outputDir`. Merely being *resolvable* is not
8
+ * enough: under npm/bun's hoisted layouts `@amritk/helpers` can sit in
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.
9
14
  */
10
15
  export declare const detectHelpersMode: (outputDir: string) => 'package' | 'embedded';
@@ -1,21 +1,45 @@
1
- import { createRequire } from 'node:module';
2
- import { resolve } from 'node:path';
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join, parse as parsePath, resolve } from 'node:path';
3
+ /** Reads a package.json's declared dependency names, or `undefined` if unreadable. */
4
+ const readDeclaredDeps = (packageJsonPath) => {
5
+ try {
6
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
7
+ return new Set([
8
+ ...Object.keys(pkg.dependencies ?? {}),
9
+ ...Object.keys(pkg.devDependencies ?? {}),
10
+ ...Object.keys(pkg.peerDependencies ?? {}),
11
+ ...Object.keys(pkg.optionalDependencies ?? {}),
12
+ ]);
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ };
3
18
  /**
4
- * Detects whether `@amritk/helpers` is resolvable from the consumer's output
5
- * directory using standard Node module resolution (walks up `node_modules`).
19
+ * Detects whether generated parsers should import `@amritk/helpers` from the
20
+ * package (`'package'`) or ship the helper sources alongside them (`'embedded'`).
6
21
  *
7
- * Returns `'package'` when generated parsers can safely
8
- * `import { ... } from '@amritk/helpers/...'` at runtime, or `'embedded'`
9
- * when the helper sources need to be shipped alongside the generated files
10
- * for the output to be self-contained.
22
+ * We select `'package'` only when `@amritk/helpers` is a *declared* dependency
23
+ * (dependencies / devDependencies / peer / optional) of the target project's
24
+ * nearest `package.json` above `outputDir`. Merely being *resolvable* is not
25
+ * enough: under npm/bun's hoisted layouts `@amritk/helpers` can sit in
26
+ * `node_modules` purely as a transitive dependency of `@amritk/mjst`, which
27
+ * makes `import '@amritk/helpers/...'` work locally but break under
28
+ * pnpm/isolated installs where undeclared packages are not reachable. Requiring
29
+ * a declaration keeps auto-detected `'package'` output portable across install
30
+ * layouts; everything else falls back to the self-contained `'embedded'` mode.
11
31
  */
12
32
  export const detectHelpersMode = (outputDir) => {
13
- const req = createRequire(resolve(outputDir, 'noop.js'));
14
- try {
15
- req.resolve('@amritk/helpers/is-object');
16
- return 'package';
17
- }
18
- catch {
19
- return 'embedded';
33
+ let dir = resolve(outputDir);
34
+ const { root } = parsePath(dir);
35
+ while (true) {
36
+ const declared = readDeclaredDeps(join(dir, 'package.json'));
37
+ if (declared) {
38
+ return declared.has('@amritk/helpers') ? 'package' : 'embedded';
39
+ }
40
+ if (dir === root)
41
+ break;
42
+ dir = dirname(dir);
20
43
  }
44
+ return 'embedded';
21
45
  };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Usage text printed for `--help`/`-h` and when the CLI is invoked with no
3
+ * arguments. Kept as a hand-written constant because the config schema that
4
+ * documents these flags (config.schema.json) is not shipped in the published
5
+ * package; the help-text test cross-checks that every flag is listed.
6
+ */
7
+ export declare const HELP_TEXT = "mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect\n --export <name> Which export of the schema module to use when --input is not json\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --import-ext <ext> Extension on generated relative imports: ts (default) or js\n --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)\n --root-type <name> Root type name for a single --schema run (default: schema title or filename)\n --type-suffix <s> Suffix appended to every $ref-derived type name\n --banner [text] Prepend a header comment to every generated file\n --readonly Emit deeply readonly type definitions\n\nValidation:\n --strict Throw on type/shape mismatches instead of coercing to defaults\n --strip-unknown Silently drop undeclared input keys at every nesting level\n --log-warnings console.warn on input keys not declared in the schema\n\nMisc:\n --config <path> JSON config file with the same keys; CLI flags take precedence\n --version, -v Print the CLI version\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Usage text printed for `--help`/`-h` and when the CLI is invoked with no
3
+ * arguments. Kept as a hand-written constant because the config schema that
4
+ * documents these flags (config.schema.json) is not shipped in the published
5
+ * package; the help-text test cross-checks that every flag is listed.
6
+ */
7
+ export const HELP_TEXT = `mjst — generate TypeScript parsers and type definitions from JSON Schemas
8
+
9
+ Usage:
10
+ mjst --schema <path> --out-dir <dir> [options]
11
+ mjst --schema-dir <dir> --out-dir <dir> [options]
12
+ mjst --schema <path> --out-file <file> --types-only [options]
13
+
14
+ Input:
15
+ --schema <path> Schema to process: a JSON Schema file, or a module when --input is set
16
+ --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)
17
+ --input <format> Schema source format: json (default), typebox, zod, valibot, effect
18
+ --export <name> Which export of the schema module to use when --input is not json
19
+
20
+ Output:
21
+ --out-dir <dir> Output directory for the generated files
22
+ --out-file <file> Single-file output instead of a directory (requires --types-only)
23
+ --types-only Generate type definitions only, without parser functions
24
+ --build Compile the generated files to .js/.d.ts (implies --import-ext js)
25
+ --import-ext <ext> Extension on generated relative imports: ts (default) or js
26
+ --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)
27
+ --root-type <name> Root type name for a single --schema run (default: schema title or filename)
28
+ --type-suffix <s> Suffix appended to every $ref-derived type name
29
+ --banner [text] Prepend a header comment to every generated file
30
+ --readonly Emit deeply readonly type definitions
31
+
32
+ Validation:
33
+ --strict Throw on type/shape mismatches instead of coercing to defaults
34
+ --strip-unknown Silently drop undeclared input keys at every nesting level
35
+ --log-warnings console.warn on input keys not declared in the schema
36
+
37
+ Misc:
38
+ --config <path> JSON config file with the same keys; CLI flags take precedence
39
+ --version, -v Print the CLI version
40
+ --help, -h Print this help
41
+
42
+ Docs: https://github.com/amritk/mjst/tree/main/packages/cli#readme
43
+ `;
@@ -3,6 +3,7 @@ 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
5
  const isHelpersMode = (value) => value === 'package' || value === 'embedded';
6
+ const isImportExt = (value) => value === 'js' || value === 'ts';
6
7
  /**
7
8
  * Loads a JSON config file and returns the relevant CLI config properties.
8
9
  * The config file should have the same keys as the CLI flags (schema, outDir).
@@ -29,6 +30,8 @@ export const loadConfig = async (configPath) => {
29
30
  ...(typeof obj['stripUnknown'] === 'boolean' && { stripUnknown: obj['stripUnknown'] }),
30
31
  ...(typeof obj['readonly'] === 'boolean' && { readonly: obj['readonly'] }),
31
32
  ...(isHelpersMode(obj['helpers']) && { helpers: obj['helpers'] }),
33
+ ...(isImportExt(obj['importExt']) && { importExt: obj['importExt'] }),
34
+ ...(typeof obj['rootType'] === 'string' && { rootType: obj['rootType'] }),
32
35
  ...(typeof obj['typeSuffix'] === 'string' && { typeSuffix: obj['typeSuffix'] }),
33
36
  // `banner` is a boolean toggle or a custom header string, mirroring the CLI flag.
34
37
  ...((typeof obj['banner'] === 'boolean' || typeof obj['banner'] === 'string') && { banner: obj['banner'] }),
@@ -18,6 +18,8 @@ const VALUE_KEYS = new Set([
18
18
  'export',
19
19
  'helpers',
20
20
  'typeSuffix',
21
+ 'importExt',
22
+ 'rootType',
21
23
  ]);
22
24
  // Recognized flags that don't map into CliConfig because they're consumed
23
25
  // earlier in the pipeline (e.g. `--config` is read by `extractConfigPath` to
@@ -31,6 +33,11 @@ const parseHelpersValue = (value) => {
31
33
  return value;
32
34
  return undefined;
33
35
  };
36
+ const parseImportExtValue = (value) => {
37
+ if (value === 'js' || value === 'ts')
38
+ return value;
39
+ return undefined;
40
+ };
34
41
  const parseInputValue = (value) => SOURCE_FORMATS.includes(value) ? value : undefined;
35
42
  /** Assigns a value-flag onto the config. Returns false for unknown keys. */
36
43
  const assignValue = (config, key, value) => {
@@ -53,6 +60,9 @@ const assignValue = (config, key, value) => {
53
60
  case 'typeSuffix':
54
61
  config.typeSuffix = value;
55
62
  return true;
63
+ case 'rootType':
64
+ config.rootType = value;
65
+ return true;
56
66
  case 'input': {
57
67
  const parsed = parseInputValue(value);
58
68
  if (!parsed) {
@@ -69,6 +79,14 @@ const assignValue = (config, key, value) => {
69
79
  config.helpers = parsed;
70
80
  return true;
71
81
  }
82
+ case 'importExt': {
83
+ const parsed = parseImportExtValue(value);
84
+ if (!parsed) {
85
+ throw new Error(`Invalid --import-ext value "${value}". Expected one of: js, ts.`);
86
+ }
87
+ config.importExt = parsed;
88
+ return true;
89
+ }
72
90
  default:
73
91
  return false;
74
92
  }
@@ -0,0 +1,10 @@
1
+ import type { CliConfig } from './cli-config.js';
2
+ /**
3
+ * Resolves the effective extension for relative import specifiers in the
4
+ * generated output. An explicit `importExt` (flag or config file) always wins.
5
+ * Otherwise the default is `'ts'` — the literal on-disk paths, loadable by
6
+ * Bun, Node's type stripping, and tsc with `allowImportingTsExtensions` —
7
+ * except under `build`, where tsc must compile the sources and can only emit
8
+ * from `'js'` specifiers.
9
+ */
10
+ export declare const resolveImportExt: (config: Partial<CliConfig>) => 'js' | 'ts';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Resolves the effective extension for relative import specifiers in the
3
+ * generated output. An explicit `importExt` (flag or config file) always wins.
4
+ * Otherwise the default is `'ts'` — the literal on-disk paths, loadable by
5
+ * Bun, Node's type stripping, and tsc with `allowImportingTsExtensions` —
6
+ * except under `build`, where tsc must compile the sources and can only emit
7
+ * from `'js'` specifiers.
8
+ */
9
+ export const resolveImportExt = (config) => config.importExt ?? (config.build ? 'js' : 'ts');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/mjst",
3
- "version": "0.7.16",
3
+ "version": "0.9.0",
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.14",
45
- "@amritk/generate-parsers": "0.13.0",
44
+ "@amritk/adapters": "0.2.16",
45
+ "@amritk/generate-parsers": "0.15.0",
46
46
  "@amritk/generate-markdown": "0.4.1",
47
- "@amritk/helpers": "0.10.3"
47
+ "@amritk/helpers": "0.12.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "json-schema-typed": "^8.0.1"