@amritk/mjst 0.8.0 → 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>
@@ -235,10 +259,19 @@ npx mjst --config ./mjst.config.json
235
259
  <td>🔗 <code>importExt</code></td>
236
260
  <td><code>--import-ext &lt;ext&gt;</code></td>
237
261
  <td><code>string</code></td>
238
- <td align="center"><code>"js"</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>
239
272
  </tr>
240
273
  <tr>
241
- <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). 'js' (default) is the standard TS NodeNext form ('./x.js' resolving to a sibling x.ts), accepted by tsc, Bun, and bundlers, and required by 'build'. 'ts' emits the literal on-disk paths so the generated .ts sources run directly under Node's type stripping (Node 22.6+ with --experimental-strip-types, on by default from Node 23). Incompatible with 'build' — tsc refuses to emit from .ts specifiers.<br><strong>Allowed:</strong> <code>"js"</code>, <code>"ts"</code></td>
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>
242
275
  </tr>
243
276
  <tr>
244
277
  <td>⚙️ <code>config</code></td>
@@ -247,7 +280,7 @@ npx mjst --config ./mjst.config.json
247
280
  <td align="center"></td>
248
281
  </tr>
249
282
  <tr>
250
- <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). 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>
251
284
  </tr>
252
285
  </tbody>
253
286
  </table>
@@ -93,13 +93,22 @@ export type CliConfig = {
93
93
  * Extension emitted on every relative import specifier in the generated
94
94
  * output (cross-file `$ref` imports, the index barrel, and embedded-helper
95
95
  * imports).
96
- * - `'js'` (default): the standard TS NodeNext form (`./x.js` resolving to a
97
- * sibling `x.ts`), accepted by tsc, Bun, and bundlers, and required by
98
- * `build`.
99
- * - `'ts'`: the literal on-disk paths, so the generated `.ts` sources run
100
- * directly under Node's type stripping (Node 22.6+ with
101
- * `--experimental-strip-types`, default from Node 23). Incompatible with
102
- * `build` — tsc refuses to emit from `.ts` specifiers.
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.
103
104
  */
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;
105
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, config.importExt);
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, config.importExt);
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, config.importExt);
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,12 @@ 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
+ }
246
277
  // tsc cannot emit from `.ts` import specifiers (allowImportingTsExtensions
247
278
  // requires noEmit), so compiled output and stripping-ready sources are
248
279
  // mutually exclusive.
@@ -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
+ `;
@@ -31,6 +31,7 @@ export const loadConfig = async (configPath) => {
31
31
  ...(typeof obj['readonly'] === 'boolean' && { readonly: obj['readonly'] }),
32
32
  ...(isHelpersMode(obj['helpers']) && { helpers: obj['helpers'] }),
33
33
  ...(isImportExt(obj['importExt']) && { importExt: obj['importExt'] }),
34
+ ...(typeof obj['rootType'] === 'string' && { rootType: obj['rootType'] }),
34
35
  ...(typeof obj['typeSuffix'] === 'string' && { typeSuffix: obj['typeSuffix'] }),
35
36
  // `banner` is a boolean toggle or a custom header string, mirroring the CLI flag.
36
37
  ...((typeof obj['banner'] === 'boolean' || typeof obj['banner'] === 'string') && { banner: obj['banner'] }),
@@ -19,6 +19,7 @@ const VALUE_KEYS = new Set([
19
19
  'helpers',
20
20
  'typeSuffix',
21
21
  'importExt',
22
+ 'rootType',
22
23
  ]);
23
24
  // Recognized flags that don't map into CliConfig because they're consumed
24
25
  // earlier in the pipeline (e.g. `--config` is read by `extractConfigPath` to
@@ -59,6 +60,9 @@ const assignValue = (config, key, value) => {
59
60
  case 'typeSuffix':
60
61
  config.typeSuffix = value;
61
62
  return true;
63
+ case 'rootType':
64
+ config.rootType = value;
65
+ return true;
62
66
  case 'input': {
63
67
  const parsed = parseInputValue(value);
64
68
  if (!parsed) {
@@ -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.8.0",
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.15",
45
- "@amritk/generate-parsers": "0.14.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.11.0"
47
+ "@amritk/helpers": "0.12.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "json-schema-typed": "^8.0.1"