@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 +48 -6
- package/dist/cli-config.d.ts +22 -0
- package/dist/cli.js +48 -10
- package/dist/detect-helpers-mode.d.ts +11 -6
- package/dist/detect-helpers-mode.js +39 -15
- package/dist/help-text.d.ts +7 -0
- package/dist/help-text.js +43 -0
- package/dist/load-config.js +3 -0
- package/dist/parse-cli-args.js +18 -0
- package/dist/resolve-import-ext.d.ts +10 -0
- package/dist/resolve-import-ext.js +9 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|

|
|
10
10
|

|
|
11
11
|

|
|
12
|
-

|
|
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/
|
|
74
|
+
api/ user.ts, index.ts
|
|
75
75
|
order.json ─▶ api/order/
|
|
76
|
-
|
|
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'
|
|
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 <ext></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 <name></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 <path></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>
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -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
|
|
132
|
-
|
|
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
|
|
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`
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
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`
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
`;
|
package/dist/load-config.js
CHANGED
|
@@ -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'] }),
|
package/dist/parse-cli-args.js
CHANGED
|
@@ -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.
|
|
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.
|
|
45
|
-
"@amritk/generate-parsers": "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.
|
|
47
|
+
"@amritk/helpers": "0.12.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"json-schema-typed": "^8.0.1"
|