@amritk/mjst 0.8.0 → 0.10.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 +82 -8
- package/dist/cli-config.d.ts +16 -7
- package/dist/cli.js +53 -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/lint/ruleset-loader.d.ts +5 -0
- package/dist/lint/ruleset-loader.js +31 -0
- package/dist/lint/run.d.ts +15 -0
- package/dist/lint/run.js +176 -0
- package/dist/load-config.js +1 -0
- package/dist/parse-cli-args.js +4 -0
- package/dist/resolve-import-ext.d.ts +10 -0
- package/dist/resolve-import-ext.js +9 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|

|
|
10
10
|

|
|
11
11
|

|
|
12
|
-

|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
Options can be supplied via CLI flags or a JSON config file. **CLI flags always take precedence over config file values.**
|
|
23
23
|
|
|
24
|
+
It also carries a `lint` subcommand — `mjst lint <files>` — a format-agnostic JSON/YAML style-guide linter powered by [`@amritk/lint`](../lint). See [Linting](#linting).
|
|
25
|
+
|
|
24
26
|
---
|
|
25
27
|
|
|
26
28
|
## Installation
|
|
@@ -71,15 +73,39 @@ npx mjst --schema-dir ./schemas --out-dir ./generated
|
|
|
71
73
|
```
|
|
72
74
|
schemas/ generated/
|
|
73
75
|
user.json ─▶ user/
|
|
74
|
-
api/
|
|
76
|
+
api/ user.ts, index.ts
|
|
75
77
|
order.json ─▶ api/order/
|
|
76
|
-
|
|
78
|
+
order.ts, index.ts
|
|
77
79
|
_helpers/ ← shared runtime helpers (embedded mode)
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
Each schema lands in its own subdirectory so generated files never collide, and in embedded
|
|
81
83
|
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.
|
|
84
|
+
imports from that single shared location. `--build` compiles the whole tree in place. Each
|
|
85
|
+
schema's root type is named after the schema (its `title`, else its filename in PascalCase),
|
|
86
|
+
so `user.json` yields `User` / `parseUser` and `api/order.json` yields `Order` / `parseOrder`.
|
|
87
|
+
|
|
88
|
+
### Running the generated output
|
|
89
|
+
|
|
90
|
+
By default mjst emits relative imports with the literal `.ts` extension (e.g.
|
|
91
|
+
`import { parseUser } from './user.ts'`), so the generated sources run **as-is** — no build
|
|
92
|
+
step — under:
|
|
93
|
+
|
|
94
|
+
- **Bun** (`bun run generated/index.ts`)
|
|
95
|
+
- **Node ≥ 22.6** type stripping (`node generated/index.ts`; unflagged from 22.18/23, else pass
|
|
96
|
+
`--experimental-strip-types`)
|
|
97
|
+
- **tsc**, provided your `tsconfig.json` sets `"allowImportingTsExtensions": true`
|
|
98
|
+
|
|
99
|
+
If you'd rather compile the output first, pass `--build` (mjst switches the imports to the
|
|
100
|
+
NodeNext `./user.js` form and runs `tsc` for you), or generate with `--import-ext js` to emit
|
|
101
|
+
the compile-ready form without building. `--import-ext ts` together with `--build` is rejected,
|
|
102
|
+
since tsc cannot emit from `.ts` specifiers.
|
|
103
|
+
|
|
104
|
+
> [!NOTE]
|
|
105
|
+
> **Breaking change (0.9.0):** the root type is no longer always named `Document`. It is derived
|
|
106
|
+
> from the schema `title`, falling back to the schema filename (`spec-plan.json` → `SpecPlan`,
|
|
107
|
+
> giving `parseSpecPlan` / `validateSpecPlanShape`). Pass `--root-type <Name>` to override it for
|
|
108
|
+
> a single `--schema` run. Update any imports that referenced `Document` / `parseDocument`.
|
|
83
109
|
|
|
84
110
|
### Config file
|
|
85
111
|
|
|
@@ -92,6 +118,45 @@ npx mjst --config ./mjst.config.json
|
|
|
92
118
|
|
|
93
119
|
---
|
|
94
120
|
|
|
121
|
+
## Linting
|
|
122
|
+
|
|
123
|
+
`mjst lint` lints JSON/YAML documents against a ruleset — JSON Schema validation and custom style rules, with exact `line:column` findings. It has its own flags (and its own `--help`), independent of the generation flags above.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Lint files against an explicit ruleset
|
|
127
|
+
npx mjst lint "**/*.{yaml,json}" -r .lint.yaml
|
|
128
|
+
|
|
129
|
+
# Auto-discover a .lint.* ruleset by walking up from each file
|
|
130
|
+
npx mjst lint config/
|
|
131
|
+
|
|
132
|
+
# Lint piped input
|
|
133
|
+
cat service.yaml | npx mjst lint --stdin-filepath service.yaml
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Findings are printed as a compact, editor-clickable report:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
config/service.yaml:2:7 error config-schema /port must be integer
|
|
140
|
+
|
|
141
|
+
✖ 1 problem(s) (1 error(s), 0 warning(s))
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
With no `-r`, mjst discovers a `.lint.{yaml,yml,json,js,mjs}` ruleset by walking up from each linted file. A ruleset maps a JSONPath (`given`) to a function (`then`) — `schema` (JSON Schema), `casing`, `pattern`, `truthy`, and more, plus your own. The [`@amritk/lint` README](../lint#ruleset-format) documents the ruleset format and built-in functions. For machine-readable output (JSON, SARIF, …), call the [`@amritk/lint`](../lint) library's `lintDocument`, which returns structured findings.
|
|
145
|
+
|
|
146
|
+
| Flag | Description |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `<documents..>` | Files or globs to lint (positional). |
|
|
149
|
+
| `-r`, `--ruleset` | Path to a ruleset file. Defaults to `.lint.*` discovery. |
|
|
150
|
+
| `-F`, `--fail-severity` | Minimum severity that fails the run: `error` (default), `warn`, `info`, `hint`. |
|
|
151
|
+
| `-D`, `--display-only-failures` | Only show findings at or above `--fail-severity`. |
|
|
152
|
+
| `--stdin-filepath` | Path to associate with piped input (labels findings and enables ruleset discovery). |
|
|
153
|
+
| `--concurrency` | Max documents linted in parallel (default `8`). |
|
|
154
|
+
| `-q`, `--quiet` | Suppress the findings report (the exit code still reflects findings). |
|
|
155
|
+
|
|
156
|
+
The exit code is `0` when no finding reaches `--fail-severity`, `1` when one does, and `2` on a usage error. Run `mjst lint --help` for the full list.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
95
160
|
## Configuration reference
|
|
96
161
|
|
|
97
162
|
<!-- config-table-start -->
|
|
@@ -220,7 +285,7 @@ npx mjst --config ./mjst.config.json
|
|
|
220
285
|
<td align="center"></td>
|
|
221
286
|
</tr>
|
|
222
287
|
<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'
|
|
288
|
+
<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
289
|
</tr>
|
|
225
290
|
<tr>
|
|
226
291
|
<td>🏷️ <code>typeSuffix</code></td>
|
|
@@ -235,10 +300,19 @@ npx mjst --config ./mjst.config.json
|
|
|
235
300
|
<td>🔗 <code>importExt</code></td>
|
|
236
301
|
<td><code>--import-ext <ext></code></td>
|
|
237
302
|
<td><code>string</code></td>
|
|
238
|
-
<td align="center"><code>"
|
|
303
|
+
<td align="center"><code>"ts"</code></td>
|
|
304
|
+
</tr>
|
|
305
|
+
<tr>
|
|
306
|
+
<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>
|
|
307
|
+
</tr>
|
|
308
|
+
<tr>
|
|
309
|
+
<td>🌱 <code>rootType</code></td>
|
|
310
|
+
<td><code>--root-type <name></code></td>
|
|
311
|
+
<td><code>string</code></td>
|
|
312
|
+
<td align="center"></td>
|
|
239
313
|
</tr>
|
|
240
314
|
<tr>
|
|
241
|
-
<td colspan="4">
|
|
315
|
+
<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
316
|
</tr>
|
|
243
317
|
<tr>
|
|
244
318
|
<td>⚙️ <code>config</code></td>
|
|
@@ -247,7 +321,7 @@ npx mjst --config ./mjst.config.json
|
|
|
247
321
|
<td align="center"></td>
|
|
248
322
|
</tr>
|
|
249
323
|
<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>
|
|
324
|
+
<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
325
|
</tr>
|
|
252
326
|
</tbody>
|
|
253
327
|
</table>
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -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
|
-
* - `'
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* `
|
|
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
|
|
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, config
|
|
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
|
|
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, config
|
|
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);
|
|
@@ -230,10 +249,28 @@ const runRecursive = async (config, schemaDir, outputDir) => {
|
|
|
230
249
|
const run = async () => {
|
|
231
250
|
// Skip the first two args (node executable and script path)
|
|
232
251
|
const args = process.argv.slice(2);
|
|
252
|
+
// `mjst lint <files>` delegates to the linter, which owns its own flag parsing
|
|
253
|
+
// and `--help` (yargs) — so this must run before the shared version/help checks,
|
|
254
|
+
// otherwise `mjst lint --help` would print the codegen help instead.
|
|
255
|
+
if (args[0] === 'lint') {
|
|
256
|
+
const { run: lintRun } = await import('./lint/run.js');
|
|
257
|
+
const result = await lintRun(args.slice(1));
|
|
258
|
+
if (result.stdout)
|
|
259
|
+
process.stdout.write(result.stdout);
|
|
260
|
+
if (result.stderr)
|
|
261
|
+
process.stderr.write(result.stderr);
|
|
262
|
+
process.exit(result.code);
|
|
263
|
+
}
|
|
233
264
|
if (isVersionRequest(args)) {
|
|
234
265
|
console.log(await readVersion());
|
|
235
266
|
return;
|
|
236
267
|
}
|
|
268
|
+
// Bare `mjst` and `mjst --help` both print usage rather than erroring on a
|
|
269
|
+
// missing --out-dir — a first-run user needs to see the flags, not a failure.
|
|
270
|
+
if (args.length === 0 || isHelpRequest(args)) {
|
|
271
|
+
console.log(HELP_TEXT);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
237
274
|
const configPath = extractConfigPath(args);
|
|
238
275
|
// Start with config file values if provided, then overlay CLI flags on top
|
|
239
276
|
const fileConfig = configPath ? await loadConfig(configPath) : {};
|
|
@@ -243,6 +280,12 @@ const run = async () => {
|
|
|
243
280
|
console.error('Error: provide only one of --out-dir or --out-file, not both.');
|
|
244
281
|
process.exit(1);
|
|
245
282
|
}
|
|
283
|
+
// --root-type names a single root; under --schema-dir each schema derives its
|
|
284
|
+
// own root name, so a single override would be ambiguous.
|
|
285
|
+
if (config.rootType && config.schemaDir) {
|
|
286
|
+
console.error('Error: --root-type cannot be combined with --schema-dir; each schema derives its own root type.');
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
246
289
|
// tsc cannot emit from `.ts` import specifiers (allowImportingTsExtensions
|
|
247
290
|
// requires noEmit), so compiled output and stripping-ready sources are
|
|
248
291
|
// mutually exclusive.
|
|
@@ -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
|
+
`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type RulesetDefinition } from '@amritk/lint';
|
|
2
|
+
/** Loads a ruleset definition from a YAML, JSON, or JS/MJS file. */
|
|
3
|
+
export declare const loadRuleset: (path: string) => Promise<RulesetDefinition>;
|
|
4
|
+
/** Walks up from a directory looking for a `.lint.*` ruleset file. */
|
|
5
|
+
export declare const discoverRuleset: (startDir: string) => string | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { parseWithPointers } from '@amritk/lint';
|
|
6
|
+
const RULESET_FILENAMES = ['.lint.yaml', '.lint.yml', '.lint.json', '.lint.js', '.lint.mjs'];
|
|
7
|
+
/** Loads a ruleset definition from a YAML, JSON, or JS/MJS file. */
|
|
8
|
+
export const loadRuleset = async (path) => {
|
|
9
|
+
const absolute = isAbsolute(path) ? path : resolve(process.cwd(), path);
|
|
10
|
+
if (/\.(c|m)?js$/.test(absolute)) {
|
|
11
|
+
const module = await import(pathToFileURL(absolute).href);
|
|
12
|
+
return (module.default ?? module);
|
|
13
|
+
}
|
|
14
|
+
const content = await readFile(absolute, 'utf8');
|
|
15
|
+
return parseWithPointers(content).data;
|
|
16
|
+
};
|
|
17
|
+
/** Walks up from a directory looking for a `.lint.*` ruleset file. */
|
|
18
|
+
export const discoverRuleset = (startDir) => {
|
|
19
|
+
let dir = resolve(startDir);
|
|
20
|
+
while (true) {
|
|
21
|
+
for (const name of RULESET_FILENAMES) {
|
|
22
|
+
const candidate = join(dir, name);
|
|
23
|
+
if (existsSync(candidate))
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(dir);
|
|
27
|
+
if (parent === dir)
|
|
28
|
+
return undefined;
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The outcome of a {@link run}: exit code plus the text it would print. */
|
|
2
|
+
export type RunResult = {
|
|
3
|
+
code: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Runs the linter over `argv`, returning the exit code and the text it would
|
|
9
|
+
* print (rather than writing to the process streams) so it can be driven
|
|
10
|
+
* in-process by tests. `stdin` supplies the piped document when there are no file
|
|
11
|
+
* targets; when omitted it is read from `process.stdin`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const run: (argv: string[], options?: {
|
|
14
|
+
stdin?: string;
|
|
15
|
+
}) => Promise<RunResult>;
|
package/dist/lint/run.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { DiagnosticSeverity, lintDocument, validateRuleset, } from '@amritk/lint';
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
import yargs from 'yargs';
|
|
6
|
+
import { discoverRuleset, loadRuleset } from './ruleset-loader.js';
|
|
7
|
+
const SEVERITY_BY_NAME = {
|
|
8
|
+
error: DiagnosticSeverity.Error,
|
|
9
|
+
warn: DiagnosticSeverity.Warning,
|
|
10
|
+
info: DiagnosticSeverity.Information,
|
|
11
|
+
hint: DiagnosticSeverity.Hint,
|
|
12
|
+
};
|
|
13
|
+
const SEVERITY_LABEL = ['error', 'warning', 'info', 'hint'];
|
|
14
|
+
/**
|
|
15
|
+
* Renders findings as a compact, dependency-free report — one `file:line:col`
|
|
16
|
+
* line per finding (1-based, editor-clickable), then a count summary. Structured
|
|
17
|
+
* output (JSON, SARIF, …) is a consumer concern: use the `@amritk/lint` library's
|
|
18
|
+
* `lintDocument`, which returns `IDiagnostic[]`.
|
|
19
|
+
*/
|
|
20
|
+
const formatReport = (findings) => {
|
|
21
|
+
if (findings.length === 0)
|
|
22
|
+
return 'No problems found\n';
|
|
23
|
+
const lines = findings.map((d) => {
|
|
24
|
+
const loc = `${d.source ?? '<stdin>'}:${d.range.start.line + 1}:${d.range.start.character + 1}`;
|
|
25
|
+
return `${loc} ${SEVERITY_LABEL[d.severity] ?? 'error'} ${d.code} ${d.message}`;
|
|
26
|
+
});
|
|
27
|
+
const errors = findings.filter((d) => d.severity === DiagnosticSeverity.Error).length;
|
|
28
|
+
const warnings = findings.filter((d) => d.severity === DiagnosticSeverity.Warning).length;
|
|
29
|
+
lines.push('', `✖ ${findings.length} problem(s) (${errors} error(s), ${warnings} warning(s))`);
|
|
30
|
+
return `${lines.join('\n')}\n`;
|
|
31
|
+
};
|
|
32
|
+
const readStdin = async () => {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
for await (const chunk of process.stdin)
|
|
35
|
+
chunks.push(chunk);
|
|
36
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
37
|
+
};
|
|
38
|
+
const filterBySeverity = (results, failSeverity) => results.filter((result) => result.severity <= failSeverity);
|
|
39
|
+
/**
|
|
40
|
+
* Maps `items` through `worker` with at most `limit` running at once, preserving
|
|
41
|
+
* input order in the result. Lets the CLI lint a directory of documents
|
|
42
|
+
* concurrently instead of strictly one at a time, without unbounded parallelism.
|
|
43
|
+
*/
|
|
44
|
+
const mapWithConcurrency = async (items, limit, worker) => {
|
|
45
|
+
const results = new Array(items.length);
|
|
46
|
+
let next = 0;
|
|
47
|
+
const runners = new Array(Math.max(1, Math.min(limit, items.length))).fill(0).map(async () => {
|
|
48
|
+
while (true) {
|
|
49
|
+
const index = next++;
|
|
50
|
+
if (index >= items.length)
|
|
51
|
+
return;
|
|
52
|
+
results[index] = await worker(items[index]);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
await Promise.all(runners);
|
|
56
|
+
return results;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Runs the linter over `argv`, returning the exit code and the text it would
|
|
60
|
+
* print (rather than writing to the process streams) so it can be driven
|
|
61
|
+
* in-process by tests. `stdin` supplies the piped document when there are no file
|
|
62
|
+
* targets; when omitted it is read from `process.stdin`.
|
|
63
|
+
*/
|
|
64
|
+
export const run = async (argv, options = {}) => {
|
|
65
|
+
const out = [];
|
|
66
|
+
const err = [];
|
|
67
|
+
const parsed = (await yargs(argv)
|
|
68
|
+
.scriptName('lint')
|
|
69
|
+
.parserConfiguration({ 'greedy-arrays': false })
|
|
70
|
+
.usage('$0 [documents..]', 'Lint JSON/YAML documents against a ruleset')
|
|
71
|
+
.positional('documents', { describe: 'Documents or globs to lint', type: 'string', array: true })
|
|
72
|
+
.option('ruleset', { alias: 'r', type: 'string', describe: 'Path to a ruleset file' })
|
|
73
|
+
.option('encoding', { type: 'string', default: 'utf8', describe: 'Input encoding' })
|
|
74
|
+
.option('fail-severity', { alias: 'F', type: 'string', default: 'error', choices: Object.keys(SEVERITY_BY_NAME) })
|
|
75
|
+
.option('display-only-failures', { alias: 'D', type: 'boolean', default: false })
|
|
76
|
+
.option('verbose', { type: 'boolean', default: false })
|
|
77
|
+
.option('quiet', { alias: 'q', type: 'boolean', default: false })
|
|
78
|
+
.option('stdin-filepath', {
|
|
79
|
+
type: 'string',
|
|
80
|
+
describe: 'Path to associate with stdin input (labels findings and enables ruleset discovery)',
|
|
81
|
+
})
|
|
82
|
+
.option('concurrency', {
|
|
83
|
+
type: 'number',
|
|
84
|
+
default: 8,
|
|
85
|
+
describe: 'Maximum number of documents to lint in parallel',
|
|
86
|
+
})
|
|
87
|
+
.help()
|
|
88
|
+
.alias('help', 'h')
|
|
89
|
+
.parse());
|
|
90
|
+
const failSeverity = SEVERITY_BY_NAME[parsed.failSeverity] ?? DiagnosticSeverity.Error;
|
|
91
|
+
const reportRulesetProblems = (definition, label) => {
|
|
92
|
+
if (parsed.quiet)
|
|
93
|
+
return;
|
|
94
|
+
for (const problem of validateRuleset(definition)) {
|
|
95
|
+
const at = problem.path.length > 0 ? ` (at ${problem.path.join('.')})` : '';
|
|
96
|
+
err.push(`warning: ruleset ${label}: ${problem.message}${at}\n`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const discoverAndLoad = async (dir) => {
|
|
100
|
+
const discovered = discoverRuleset(dir);
|
|
101
|
+
if (!discovered)
|
|
102
|
+
return undefined;
|
|
103
|
+
const definition = await loadRuleset(discovered);
|
|
104
|
+
reportRulesetProblems(definition, discovered);
|
|
105
|
+
return { definition, basePath: dirname(discovered) };
|
|
106
|
+
};
|
|
107
|
+
let rulesetDefinition;
|
|
108
|
+
let rulesetBasePath;
|
|
109
|
+
if (parsed.ruleset) {
|
|
110
|
+
rulesetDefinition = await loadRuleset(parsed.ruleset);
|
|
111
|
+
rulesetBasePath = dirname(isAbsolute(parsed.ruleset) ? parsed.ruleset : resolve(process.cwd(), parsed.ruleset));
|
|
112
|
+
reportRulesetProblems(rulesetDefinition, parsed.ruleset);
|
|
113
|
+
}
|
|
114
|
+
const targets = await fg(parsed.documents ?? [], { dot: true, onlyFiles: true });
|
|
115
|
+
const allResults = [];
|
|
116
|
+
if (targets.length === 0) {
|
|
117
|
+
if (options.stdin === undefined && process.stdin.isTTY) {
|
|
118
|
+
return { code: 2, stdout: '', stderr: 'No documents provided.\n' };
|
|
119
|
+
}
|
|
120
|
+
const content = options.stdin ?? (await readStdin());
|
|
121
|
+
// With --stdin-filepath, label findings with that path and discover a
|
|
122
|
+
// `.lint.*` ruleset by walking up from its directory.
|
|
123
|
+
const stdinPath = parsed.stdinFilepath;
|
|
124
|
+
let definition = rulesetDefinition;
|
|
125
|
+
let basePath = rulesetBasePath;
|
|
126
|
+
if (!definition && !parsed.ruleset && stdinPath) {
|
|
127
|
+
const discovered = await discoverAndLoad(dirname(stdinPath));
|
|
128
|
+
if (discovered)
|
|
129
|
+
({ definition, basePath } = discovered);
|
|
130
|
+
}
|
|
131
|
+
const opts = {
|
|
132
|
+
...(definition ? { ruleset: definition } : {}),
|
|
133
|
+
...(basePath !== undefined ? { rulesetBasePath: basePath } : {}),
|
|
134
|
+
source: stdinPath ?? '<stdin>',
|
|
135
|
+
};
|
|
136
|
+
allResults.push(...(await lintDocument(content, opts)));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Cache discovered/loaded rulesets by directory so a directory of documents
|
|
140
|
+
// that share a `.lint.*` file parses it once, not once per file.
|
|
141
|
+
const rulesetCache = new Map();
|
|
142
|
+
const perFile = await mapWithConcurrency(targets, parsed.concurrency, async (file) => {
|
|
143
|
+
const content = await readFile(file, parsed.encoding);
|
|
144
|
+
let definition = rulesetDefinition;
|
|
145
|
+
let basePath = rulesetBasePath;
|
|
146
|
+
if (!definition && !parsed.ruleset) {
|
|
147
|
+
const dir = dirname(file);
|
|
148
|
+
let discovered = rulesetCache.get(dir);
|
|
149
|
+
if (!rulesetCache.has(dir)) {
|
|
150
|
+
discovered = await discoverAndLoad(dir);
|
|
151
|
+
rulesetCache.set(dir, discovered);
|
|
152
|
+
}
|
|
153
|
+
if (discovered)
|
|
154
|
+
({ definition, basePath } = discovered);
|
|
155
|
+
}
|
|
156
|
+
const opts = definition
|
|
157
|
+
? {
|
|
158
|
+
ruleset: definition,
|
|
159
|
+
...(basePath !== undefined ? { rulesetBasePath: basePath } : {}),
|
|
160
|
+
source: file,
|
|
161
|
+
}
|
|
162
|
+
: { source: file };
|
|
163
|
+
return lintDocument(content, opts);
|
|
164
|
+
});
|
|
165
|
+
for (const remaining of perFile)
|
|
166
|
+
allResults.push(...remaining);
|
|
167
|
+
}
|
|
168
|
+
const displayed = parsed.displayOnlyFailures ? filterBySeverity(allResults, failSeverity) : allResults;
|
|
169
|
+
if (!parsed.quiet)
|
|
170
|
+
out.push(formatReport(displayed));
|
|
171
|
+
return {
|
|
172
|
+
code: filterBySeverity(allResults, failSeverity).length > 0 ? 1 : 0,
|
|
173
|
+
stdout: out.join(''),
|
|
174
|
+
stderr: err.join(''),
|
|
175
|
+
};
|
|
176
|
+
};
|
package/dist/load-config.js
CHANGED
|
@@ -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'] }),
|
package/dist/parse-cli-args.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,12 +41,16 @@
|
|
|
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
|
+
"@amritk/lint": "0.1.0",
|
|
49
|
+
"fast-glob": "^3.3.2",
|
|
50
|
+
"yargs": "^17.7.2"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
|
-
"json-schema-typed": "^8.0.1"
|
|
53
|
+
"json-schema-typed": "^8.0.1",
|
|
54
|
+
"@types/yargs": "^17.0.33"
|
|
51
55
|
}
|
|
52
56
|
}
|