@amritk/mjst 0.12.2 → 0.13.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/dist/cli.js CHANGED
@@ -1,395 +1,322 @@
1
1
  #!/usr/bin/env node
2
- import { execFile } from 'node:child_process';
3
- import { mkdir, readdir, unlink, writeFile } from 'node:fs/promises';
4
- import { basename, dirname, join, relative, resolve, sep } from 'node:path';
5
- import { promisify } from 'node:util';
6
- import { buildSchema } from '@amritk/generate-parsers';
7
- import { buildValidatorSchema } from '@amritk/generate-validators';
8
- import { deriveRootTypeName } from '@amritk/helpers/derive-root-type-name';
2
+ import { execFile } from "node:child_process";
3
+ import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { buildSchema } from "@amritk/generate-parsers";
7
+ import { buildValidatorSchema } from "@amritk/generate-validators";
8
+ import { deriveRootTypeName } from "@amritk/helpers/derive-root-type-name";
9
9
  const execFileAsync = promisify(execFile);
10
- import { combineGeneratedFiles } from './combine-files.js';
11
- import { detectHelpersMode } from './detect-helpers-mode.js';
12
- import { emitExamples } from './emit-examples.js';
13
- import { HELP_TEXT } from './help-text.js';
14
- import { loadConfig } from './load-config.js';
15
- import { loadSchema } from './load-schema.js';
16
- import { parseCliArgs } from './parse-cli-args.js';
17
- import { readVersion } from './read-version.js';
18
- import { resolveImportExt } from './resolve-import-ext.js';
19
- /** True when the args request the CLI version (`version`, `--version`, or `-v`). */
20
- const isVersionRequest = (args) => args[0] === 'version' || args.includes('--version') || args.includes('-v');
21
- /** True when the args request usage help (`help`, `--help`, or `-h`). */
22
- const isHelpRequest = (args) => args[0] === 'help' || args.includes('--help') || args.includes('-h');
23
- /** The schema's base filename without extension, used to derive the root type name. */
24
- const schemaBaseName = (schemaPath) => basename(schemaPath).replace(/\.[^.]+$/, '');
25
- /**
26
- * Resolves and reports the helpers mode. An explicit `--helpers` wins; otherwise
27
- * it is auto-detected from the nearest package.json. When detection falls back to
28
- * embedded, nudge the user toward declaring `@amritk/helpers` so multiple
29
- * generated trees can share a single runtime helper copy.
30
- */
10
+ import { combineGeneratedFiles } from "./combine-files.js";
11
+ import { detectHelpersMode } from "./detect-helpers-mode.js";
12
+ import { emitExamples } from "./emit-examples.js";
13
+ import { HELP_TEXT } from "./help-text.js";
14
+ import { loadConfig } from "./load-config.js";
15
+ import { loadSchema } from "./load-schema.js";
16
+ import { parseCliArgs } from "./parse-cli-args.js";
17
+ import { readVersion } from "./read-version.js";
18
+ import { resolveImportExt } from "./resolve-import-ext.js";
19
+ const isVersionRequest = (args) => args[0] === "version" || args.includes("--version") || args.includes("-v");
20
+ const isHelpRequest = (args) => args[0] === "help" || args.includes("--help") || args.includes("-h");
21
+ const schemaBaseName = (schemaPath) => basename(schemaPath).replace(/\.[^.]+$/, "");
31
22
  const resolveHelpersMode = (config, outputDir) => {
32
- const helpersMode = config.helpers ?? detectHelpersMode(outputDir);
33
- console.log(`Helpers mode: ${helpersMode}${config.helpers ? ' (explicit)' : ' (auto-detected)'}`);
34
- if (!config.helpers && helpersMode === 'embedded') {
35
- console.log('Tip: add @amritk/helpers as a dependency to share one runtime helper copy across generated trees ' +
36
- '(or pass --helpers embedded to silence this).');
37
- }
38
- return helpersMode;
23
+ const helpersMode = config.helpers ?? detectHelpersMode(outputDir);
24
+ console.log(`Helpers mode: ${helpersMode}${config.helpers ? " (explicit)" : " (auto-detected)"}`);
25
+ if (!config.helpers && helpersMode === "embedded") {
26
+ console.log("Tip: add @amritk/helpers as a dependency to share one runtime helper copy across generated trees (or pass --helpers embedded to silence this).");
27
+ }
28
+ return helpersMode;
39
29
  };
40
- /**
41
- * Extracts the --config flag value from process args before full parsing.
42
- * We need this early so we can load the config file first, then overlay CLI flags.
43
- */
44
30
  const extractConfigPath = (args) => {
45
- for (let i = 0; i < args.length; i++) {
46
- const arg = args[i];
47
- if (arg === '--config' && args[i + 1] && !args[i + 1]?.startsWith('--')) {
48
- return args[i + 1];
49
- }
50
- if (arg?.startsWith('--config=')) {
51
- return arg.slice('--config='.length);
52
- }
31
+ for (let i = 0; i < args.length; i++) {
32
+ const arg = args[i];
33
+ if (arg === "--config" && args[i + 1] && !args[i + 1]?.startsWith("--")) {
34
+ return args[i + 1];
35
+ }
36
+ if (arg?.startsWith("--config=")) {
37
+ return arg.slice("--config=".length);
53
38
  }
54
- return undefined;
39
+ }
40
+ return void 0;
55
41
  };
56
- const DEFAULT_BANNER_TEXT = 'This file was auto-generated by @amritk/mjst.\n * Do not edit it manually — your changes will be overwritten on the next generation.';
42
+ const DEFAULT_BANNER_TEXT = "This file was auto-generated by @amritk/mjst.\n * Do not edit it manually \u2014 your changes will be overwritten on the next generation.";
57
43
  const resolveBanner = (banner) => {
58
- if (!banner)
59
- return '';
60
- const text = banner === true ? DEFAULT_BANNER_TEXT : banner;
61
- return `/**\n * ${text}\n */\n\n`;
44
+ if (!banner)
45
+ return "";
46
+ const text = banner === true ? DEFAULT_BANNER_TEXT : banner;
47
+ return `/**
48
+ * ${text}
49
+ */
50
+
51
+ `;
62
52
  };
63
- /** Writes a generated file, creating any parent directories the filename implies. */
64
53
  const writeGeneratedFile = async (baseDir, filename, content) => {
65
- const filePath = join(baseDir, filename);
66
- await mkdir(dirname(filePath), { recursive: true });
67
- await writeFile(filePath, content, 'utf-8');
54
+ const filePath = join(baseDir, filename);
55
+ await mkdir(dirname(filePath), { recursive: true });
56
+ await writeFile(filePath, content, "utf-8");
68
57
  };
69
- /**
70
- * Emits fast-check arbitrary + example files for one schema (delegating to
71
- * {@link emitExamples}) and logs each written path. The files land in an
72
- * `examples/` subdirectory of `outputDir` so they never collide with the parser
73
- * files, which share the same `<name>.ts` / `index.ts` names. `subDir` mirrors a
74
- * schema's nested location under `--schema-dir`. The example sources are left out
75
- * of `--build`: they import the `fast-check` peer dependency and are meant to be
76
- * consumed by tests, not compiled alongside the runtime parsers.
77
- */
78
- const runExamples = async (config, schema, rootTypeName, outputDir, subDir = '') => {
79
- const written = await emitExamples({
80
- schema: schema,
81
- rootTypeName,
82
- outputDir,
83
- subDir,
84
- bannerPrefix: config.banner ? resolveBanner(config.banner) : '',
85
- // Spread conditionally: `exactOptionalPropertyTypes` rejects an explicit
86
- // `undefined` for the optional `typeSuffix`.
87
- ...(config.typeSuffix !== undefined ? { typeSuffix: config.typeSuffix } : {}),
88
- });
89
- for (const path of written) {
90
- console.log(`Generated example: ${path}`);
91
- }
58
+ const runExamples = async (config, schema, rootTypeName, outputDir, subDir = "") => {
59
+ const written = await emitExamples({
60
+ schema,
61
+ rootTypeName,
62
+ outputDir,
63
+ subDir,
64
+ bannerPrefix: config.banner ? resolveBanner(config.banner) : "",
65
+ // Spread conditionally: `exactOptionalPropertyTypes` rejects an explicit
66
+ // `undefined` for the optional `typeSuffix`.
67
+ ...config.typeSuffix !== void 0 ? { typeSuffix: config.typeSuffix } : {}
68
+ });
69
+ for (const path of written) {
70
+ console.log(`Generated example: ${path}`);
71
+ }
92
72
  };
93
- /**
94
- * Compiles the generated `.ts` files to `.js`/`.d.ts` with a temporary tsconfig,
95
- * then removes the intermediate sources. `tsFiles` are paths relative to `outputDir`.
96
- */
97
73
  const buildOutput = async (outputDir, tsFiles, typesOnly) => {
98
- // Write a minimal tsconfig so tsc can compile the generated files without
99
- // inheriting settings like allowImportingTsExtensions that block emission.
100
- // In 'package' mode tsc finds @amritk/helpers via the consumer's node_modules.
101
- // In 'embedded' mode the helpers live under ./_helpers and need no resolution.
102
- const tsconfigContent = JSON.stringify({
103
- compilerOptions: {
104
- target: 'ESNext',
105
- module: 'ESNext',
106
- moduleResolution: 'bundler',
107
- declaration: true,
108
- emitDeclarationOnly: typesOnly,
109
- skipLibCheck: true,
110
- },
111
- include: tsFiles.map((file) => `./${file}`),
112
- }, null, 2);
113
- const tsconfigPath = join(outputDir, '.tsconfig-mjst-build.json');
114
- await writeFile(tsconfigPath, tsconfigContent, 'utf-8');
115
- try {
116
- await execFileAsync('npx', ['tsc', '--project', tsconfigPath]);
117
- }
118
- catch (error) {
119
- const details = error;
120
- // A spawn failure (ENOENT) means the *tool* is missing, not that the code is
121
- // wrong — pointing the user at "the generated files" would send them hunting
122
- // for a compile error that doesn't exist. Report the missing executable.
123
- if (details.code === 'ENOENT') {
124
- throw new Error('Could not run `npx tsc` to compile the generated files: `npx` (or `tsc`) was not found. ' +
125
- 'Install TypeScript (e.g. `npm install -D typescript`) or drop --build to skip compilation.');
126
- }
127
- // Otherwise tsc ran and reported real errors on stdout; surface them so the
128
- // user sees *what* failed instead of a bare "compilation failed".
129
- const output = [details.stdout, details.stderr].filter(Boolean).join('\n').trim();
130
- throw new Error(`TypeScript compilation failed. Check the generated files for errors.${output ? `\n\n${output}` : ''}`);
131
- }
132
- finally {
133
- await unlink(tsconfigPath);
134
- }
135
- // Remove the intermediate .ts files now that .js and .d.ts have been produced
136
- for (const file of tsFiles) {
137
- await unlink(join(outputDir, file));
138
- console.log(`Built: ${file.replace(/\.ts$/, '.js')}`);
139
- console.log(`Built: ${file.replace(/\.ts$/, '.d.ts')}`);
140
- }
141
- console.log(`\nTotal files built: ${tsFiles.length * 2}`);
74
+ const tsconfigContent = JSON.stringify({
75
+ compilerOptions: {
76
+ target: "ESNext",
77
+ module: "ESNext",
78
+ moduleResolution: "bundler",
79
+ declaration: true,
80
+ emitDeclarationOnly: typesOnly,
81
+ skipLibCheck: true
82
+ },
83
+ include: tsFiles.map((file) => `./${file}`)
84
+ }, null, 2);
85
+ const tsconfigPath = join(outputDir, ".tsconfig-mjst-build.json");
86
+ await writeFile(tsconfigPath, tsconfigContent, "utf-8");
87
+ try {
88
+ await execFileAsync("npx", ["tsc", "--project", tsconfigPath]);
89
+ } catch (error) {
90
+ const details = error;
91
+ if (details.code === "ENOENT") {
92
+ throw new Error("Could not run `npx tsc` to compile the generated files: `npx` (or `tsc`) was not found. Install TypeScript (e.g. `npm install -D typescript`) or drop --build to skip compilation.");
93
+ }
94
+ const output = [details.stdout, details.stderr].filter(Boolean).join("\n").trim();
95
+ throw new Error(`TypeScript compilation failed. Check the generated files for errors.${output ? `
96
+
97
+ ${output}` : ""}`);
98
+ } finally {
99
+ await unlink(tsconfigPath);
100
+ }
101
+ for (const file of tsFiles) {
102
+ await unlink(join(outputDir, file));
103
+ console.log(`Built: ${file.replace(/\.ts$/, ".js")}`);
104
+ console.log(`Built: ${file.replace(/\.ts$/, ".d.ts")}`);
105
+ }
106
+ console.log(`
107
+ Total files built: ${tsFiles.length * 2}`);
142
108
  };
143
- /** Recursively collects every `*.json` file under `dir`, returning absolute paths. */
144
109
  const findJsonSchemas = async (dir) => {
145
- const entries = await readdir(dir, { withFileTypes: true });
146
- const results = [];
147
- for (const entry of entries) {
148
- const full = join(dir, entry.name);
149
- if (entry.isDirectory()) {
150
- // `node_modules` holds dependency schemas that aren't the user's, and
151
- // dot-directories (`.git`, `.cache`, ...) hold tooling state — descending
152
- // into either pulls in `.json` files the user never meant to generate from.
153
- if (entry.name === 'node_modules' || entry.name.startsWith('.'))
154
- continue;
155
- results.push(...(await findJsonSchemas(full)));
156
- }
157
- else if (entry.isFile() && entry.name.endsWith('.json')) {
158
- results.push(full);
159
- }
160
- }
161
- return results;
110
+ const entries = await readdir(dir, { withFileTypes: true });
111
+ const results = [];
112
+ for (const entry of entries) {
113
+ const full = join(dir, entry.name);
114
+ if (entry.isDirectory()) {
115
+ if (entry.name === "node_modules" || entry.name.startsWith("."))
116
+ continue;
117
+ results.push(...await findJsonSchemas(full));
118
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
119
+ results.push(full);
120
+ }
121
+ }
122
+ return results;
162
123
  };
163
- /**
164
- * Emits validator files (`validateX` / `isX`) for one schema into `validatorsSubDir`
165
- * (a path relative to `outputDir`). The validators carry the same schema-derived
166
- * filenames as the parsers, so they live in their own subdirectory to avoid
167
- * colliding. Returns the written `.ts` paths relative to `outputDir` so the caller
168
- * can fold them into a `--build` compilation.
169
- */
170
124
  const runValidators = async (config, schema, rootTypeName, outputDir, validatorsSubDir) => {
171
- const files = await buildValidatorSchema(schema, rootTypeName, config.typeSuffix);
172
- const written = [];
173
- for (const file of files) {
174
- const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
175
- const relFilename = join(validatorsSubDir, file.filename);
176
- await writeGeneratedFile(outputDir, relFilename, content);
177
- written.push(relFilename);
178
- console.log(`Generated: ${relFilename}`);
179
- }
180
- return written;
125
+ const files = await buildValidatorSchema(schema, rootTypeName, config.typeSuffix);
126
+ const written = [];
127
+ for (const file of files) {
128
+ const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
129
+ const relFilename = join(validatorsSubDir, file.filename);
130
+ await writeGeneratedFile(outputDir, relFilename, content);
131
+ written.push(relFilename);
132
+ console.log(`Generated: ${relFilename}`);
133
+ }
134
+ return written;
181
135
  };
182
- /** Generates parsers for a single schema (the original one-schema-in, one-outDir-out flow). */
183
136
  const runSingle = async (config, schemaPath, outputDir) => {
184
- const schema = await loadSchema(config, schemaPath);
185
- await mkdir(outputDir, { recursive: true });
186
- const helpersMode = resolveHelpersMode(config, outputDir);
187
- const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
188
- console.log(`Root type: ${rootTypeName}`);
189
- const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, undefined, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
190
- for (const file of files) {
191
- const isHelper = file.filename.startsWith('_helpers/');
192
- const content = config.banner && !isHelper ? resolveBanner(config.banner) + file.content : file.content;
193
- await writeGeneratedFile(outputDir, file.filename, content);
194
- console.log(`Generated: ${file.filename}`);
195
- }
196
- const validatorFiles = config.validators
197
- ? await runValidators(config, schema, rootTypeName, outputDir, 'validators')
198
- : [];
199
- if (config.examples) {
200
- await runExamples(config, schema, rootTypeName, outputDir);
201
- }
202
- if (config.build) {
203
- await buildOutput(outputDir, [...files.map((f) => f.filename), ...validatorFiles], config.typesOnly);
204
- }
205
- else {
206
- console.log(`\nTotal files generated: ${files.length + validatorFiles.length}`);
207
- }
137
+ const schema = await loadSchema(config, schemaPath);
138
+ await mkdir(outputDir, { recursive: true });
139
+ const helpersMode = resolveHelpersMode(config, outputDir);
140
+ const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
141
+ console.log(`Root type: ${rootTypeName}`);
142
+ const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
143
+ for (const file of files) {
144
+ const isHelper = file.filename.startsWith("_helpers/");
145
+ const content = config.banner && !isHelper ? resolveBanner(config.banner) + file.content : file.content;
146
+ await writeGeneratedFile(outputDir, file.filename, content);
147
+ console.log(`Generated: ${file.filename}`);
148
+ }
149
+ const validatorFiles = config.validators ? await runValidators(config, schema, rootTypeName, outputDir, "validators") : [];
150
+ if (config.examples) {
151
+ await runExamples(config, schema, rootTypeName, outputDir);
152
+ }
153
+ if (config.build) {
154
+ await buildOutput(outputDir, [...files.map((f) => f.filename), ...validatorFiles], config.typesOnly);
155
+ } else {
156
+ console.log(`
157
+ Total files generated: ${files.length + validatorFiles.length}`);
158
+ }
208
159
  };
209
- /**
210
- * Generates a single self-contained file from one schema, concatenating every
211
- * generated definition (and dropping the cross-file imports that are no longer
212
- * needed) into `outFilePath`. Currently used for types-only output.
213
- */
214
160
  const runSingleFile = async (config, schemaPath, outFilePath) => {
215
- const schema = await loadSchema(config, schemaPath);
216
- const outputDir = dirname(outFilePath);
217
- await mkdir(outputDir, { recursive: true });
218
- const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
219
- console.log(`Root type: ${rootTypeName}`);
220
- 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), config.caseInsensitive);
221
- const combined = combineGeneratedFiles(files);
222
- await writeFile(outFilePath, config.banner ? resolveBanner(config.banner) + combined : combined, 'utf-8');
223
- console.log(`Generated: ${relative(process.cwd(), outFilePath)}`);
224
- if (config.examples) {
225
- await runExamples(config, schema, rootTypeName, outputDir);
226
- }
227
- if (config.build) {
228
- await buildOutput(outputDir, [basename(outFilePath)], config.typesOnly);
229
- }
230
- else {
231
- console.log(`\nTotal files generated: 1`);
232
- }
161
+ const schema = await loadSchema(config, schemaPath);
162
+ const outputDir = dirname(outFilePath);
163
+ await mkdir(outputDir, { recursive: true });
164
+ const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
165
+ console.log(`Root type: ${rootTypeName}`);
166
+ const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? "package", void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
167
+ const combined = combineGeneratedFiles(files);
168
+ await writeFile(outFilePath, config.banner ? resolveBanner(config.banner) + combined : combined, "utf-8");
169
+ console.log(`Generated: ${relative(process.cwd(), outFilePath)}`);
170
+ if (config.examples) {
171
+ await runExamples(config, schema, rootTypeName, outputDir);
172
+ }
173
+ if (config.build) {
174
+ await buildOutput(outputDir, [basename(outFilePath)], config.typesOnly);
175
+ } else {
176
+ console.log(`
177
+ Total files generated: 1`);
178
+ }
233
179
  };
234
- /**
235
- * Generates parsers for every JSON Schema under `schemaDir`, mirroring the directory
236
- * layout into `outDir`. Each schema's output lands in its own subdirectory (so files
237
- * from different schemas never collide) and the runtime helpers are emitted once into a
238
- * shared `outDir/_helpers/` that every nested parser imports from.
239
- */
240
180
  const runRecursive = async (config, schemaDir, outputDir) => {
241
- if (config.input && config.input !== 'json') {
242
- console.error("Error: --schemaDir only supports JSON Schema files; remove --input or set it to 'json'.");
243
- process.exit(1);
244
- }
245
- const schemaFiles = await findJsonSchemas(schemaDir);
246
- if (schemaFiles.length === 0) {
247
- console.error(`Error: no .json schema files found under ${schemaDir}.`);
248
- process.exit(1);
249
- }
250
- await mkdir(outputDir, { recursive: true });
251
- const helpersMode = resolveHelpersMode(config, outputDir);
252
- // Helper sources are identical across schemas, so collect them by filename and
253
- // write the deduplicated set once at the output root.
254
- const sharedHelpers = new Map();
255
- const writtenTsFiles = [];
256
- for (const schemaFile of schemaFiles) {
257
- const relPath = relative(schemaDir, schemaFile);
258
- const relNoExt = relPath.slice(0, -'.json'.length);
259
- const schemaSubDir = join(outputDir, relNoExt);
260
- // Depth of the schema's output subdirectory, used to build the relative import
261
- // path back to the shared `_helpers/` at the output root (../, ../../, ...).
262
- const depth = relNoExt.split(sep).filter(Boolean).length;
263
- const helpersImportPrefix = '../'.repeat(depth);
264
- const schema = await loadSchema(config, schemaFile);
265
- const rootTypeName = deriveRootTypeName(schema, schemaBaseName(schemaFile));
266
- console.log(`\n${relPath} → ${relNoExt}/ (root type: ${rootTypeName})`);
267
- const files = await buildSchema(schema, rootTypeName, undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
268
- for (const file of files) {
269
- if (file.filename.startsWith('_helpers/')) {
270
- sharedHelpers.set(file.filename, file.content);
271
- continue;
272
- }
273
- const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
274
- await writeGeneratedFile(schemaSubDir, file.filename, content);
275
- const relativeFilename = join(relNoExt, file.filename);
276
- writtenTsFiles.push(relativeFilename);
277
- console.log(`Generated: ${relativeFilename}`);
278
- }
279
- if (config.validators) {
280
- // Mirror the parser layout under a top-level `validators/` tree so each
281
- // schema's validators sit beside their parser counterparts without sharing
282
- // a filename.
283
- const validatorFiles = await runValidators(config, schema, rootTypeName, outputDir, join('validators', relNoExt));
284
- writtenTsFiles.push(...validatorFiles);
285
- }
286
- if (config.examples) {
287
- await runExamples(config, schema, rootTypeName, outputDir, relNoExt);
288
- }
289
- }
290
- for (const [filename, content] of sharedHelpers) {
291
- await writeGeneratedFile(outputDir, filename, content);
292
- writtenTsFiles.push(filename);
293
- console.log(`Generated: ${filename}`);
294
- }
295
- if (config.build) {
296
- await buildOutput(outputDir, writtenTsFiles, config.typesOnly);
297
- }
298
- else {
299
- console.log(`\nTotal files generated: ${writtenTsFiles.length}`);
181
+ if (config.input && config.input !== "json") {
182
+ console.error("Error: --schemaDir only supports JSON Schema files; remove --input or set it to 'json'.");
183
+ process.exit(1);
184
+ }
185
+ const schemaFiles = await findJsonSchemas(schemaDir);
186
+ if (schemaFiles.length === 0) {
187
+ console.error(`Error: no .json schema files found under ${schemaDir}.`);
188
+ process.exit(1);
189
+ }
190
+ await mkdir(outputDir, { recursive: true });
191
+ const helpersMode = resolveHelpersMode(config, outputDir);
192
+ const sharedHelpers = /* @__PURE__ */ new Map();
193
+ const writtenTsFiles = [];
194
+ for (const schemaFile of schemaFiles) {
195
+ const relPath = relative(schemaDir, schemaFile);
196
+ const relNoExt = relPath.slice(0, -".json".length);
197
+ const schemaSubDir = join(outputDir, relNoExt);
198
+ const depth = relNoExt.split(sep).filter(Boolean).length;
199
+ const helpersImportPrefix = "../".repeat(depth);
200
+ const schema = await loadSchema(config, schemaFile);
201
+ const rootTypeName = deriveRootTypeName(schema, schemaBaseName(schemaFile));
202
+ console.log(`
203
+ ${relPath} \u2192 ${relNoExt}/ (root type: ${rootTypeName})`);
204
+ const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, helpersImportPrefix, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive);
205
+ for (const file of files) {
206
+ if (file.filename.startsWith("_helpers/")) {
207
+ sharedHelpers.set(file.filename, file.content);
208
+ continue;
209
+ }
210
+ const content = config.banner ? resolveBanner(config.banner) + file.content : file.content;
211
+ await writeGeneratedFile(schemaSubDir, file.filename, content);
212
+ const relativeFilename = join(relNoExt, file.filename);
213
+ writtenTsFiles.push(relativeFilename);
214
+ console.log(`Generated: ${relativeFilename}`);
215
+ }
216
+ if (config.validators) {
217
+ const validatorFiles = await runValidators(config, schema, rootTypeName, outputDir, join("validators", relNoExt));
218
+ writtenTsFiles.push(...validatorFiles);
300
219
  }
220
+ if (config.examples) {
221
+ await runExamples(config, schema, rootTypeName, outputDir, relNoExt);
222
+ }
223
+ }
224
+ for (const [filename, content] of sharedHelpers) {
225
+ await writeGeneratedFile(outputDir, filename, content);
226
+ writtenTsFiles.push(filename);
227
+ console.log(`Generated: ${filename}`);
228
+ }
229
+ if (config.build) {
230
+ await buildOutput(outputDir, writtenTsFiles, config.typesOnly);
231
+ } else {
232
+ console.log(`
233
+ Total files generated: ${writtenTsFiles.length}`);
234
+ }
301
235
  };
302
236
  const run = async () => {
303
- // Skip the first two args (node executable and script path)
304
- const args = process.argv.slice(2);
305
- // `mjst lint <files>` delegates to the linter, which owns its own flag parsing
306
- // and `--help` (yargs) — so this must run before the shared version/help checks,
307
- // otherwise `mjst lint --help` would print the codegen help instead.
308
- if (args[0] === 'lint') {
309
- const { run: lintRun } = await import('./lint/run.js');
310
- const result = await lintRun(args.slice(1));
311
- if (result.stdout)
312
- process.stdout.write(result.stdout);
313
- if (result.stderr)
314
- process.stderr.write(result.stderr);
315
- process.exit(result.code);
316
- }
317
- if (isVersionRequest(args)) {
318
- console.log(await readVersion());
319
- return;
320
- }
321
- // Bare `mjst` and `mjst --help` both print usage rather than erroring on a
322
- // missing --out-dir — a first-run user needs to see the flags, not a failure.
323
- if (args.length === 0 || isHelpRequest(args)) {
324
- console.log(HELP_TEXT);
325
- return;
326
- }
327
- const configPath = extractConfigPath(args);
328
- // Start with config file values if provided, then overlay CLI flags on top
329
- const fileConfig = configPath ? await loadConfig(configPath) : {};
330
- const cliConfig = parseCliArgs(args);
331
- const config = { ...fileConfig, ...cliConfig };
332
- if (config.outDir && config.outFile) {
333
- console.error('Error: provide only one of --out-dir or --out-file, not both.');
334
- process.exit(1);
335
- }
336
- // --root-type names a single root; under --schema-dir each schema derives its
337
- // own root name, so a single override would be ambiguous.
338
- if (config.rootType && config.schemaDir) {
339
- console.error('Error: --root-type cannot be combined with --schema-dir; each schema derives its own root type.');
340
- process.exit(1);
341
- }
342
- // tsc cannot emit from `.ts` import specifiers (allowImportingTsExtensions
343
- // requires noEmit), so compiled output and stripping-ready sources are
344
- // mutually exclusive.
345
- if (config.build && config.importExt === 'ts') {
346
- console.error('Error: --import-ext ts cannot be combined with --build. Drop --build to keep runnable .ts sources.');
347
- process.exit(1);
348
- }
349
- // Validators are runtime code (validateX / isX), so they have nothing to emit
350
- // in the type-only flows.
351
- if (config.validators && config.typesOnly) {
352
- console.error('Error: --validators cannot be combined with --types-only, which emits no runtime code.');
353
- process.exit(1);
354
- }
355
- if (config.validators && config.outFile) {
356
- console.error('Error: --validators cannot be combined with --out-file. Use --out-dir to emit validator files.');
357
- process.exit(1);
358
- }
359
- // Single-file output is the alternative to a directory of files.
360
- if (config.outFile) {
361
- if (!config.typesOnly) {
362
- console.error('Error: --out-file currently supports only --types-only output. Add --types-only or use --out-dir.');
363
- process.exit(1);
364
- }
365
- if (config.schemaDir) {
366
- console.error('Error: --out-file cannot be combined with --schema-dir; use --out-dir for recursive output.');
367
- process.exit(1);
368
- }
369
- if (!config.schema) {
370
- console.error('Error: --schema is required. Provide a path to a JSON Schema.');
371
- process.exit(1);
372
- }
373
- await runSingleFile(config, resolve(config.schema), resolve(config.outFile));
374
- return;
375
- }
376
- if (!config.outDir) {
377
- console.error('Error: --out-dir or --out-file is required. Provide an output destination for generated files.');
378
- process.exit(1);
237
+ const args = process.argv.slice(2);
238
+ if (args[0] === "lint") {
239
+ const { run: lintRun } = await import("./lint/run.js");
240
+ const result = await lintRun(args.slice(1));
241
+ if (result.stdout)
242
+ process.stdout.write(result.stdout);
243
+ if (result.stderr)
244
+ process.stderr.write(result.stderr);
245
+ process.exit(result.code);
246
+ }
247
+ if (args[0] === "compile-api") {
248
+ const { run: compileApiRun } = await import("./compile-api/run.js");
249
+ const result = await compileApiRun(args.slice(1));
250
+ if (result.stdout)
251
+ process.stdout.write(result.stdout);
252
+ if (result.stderr)
253
+ process.stderr.write(result.stderr);
254
+ process.exit(result.code);
255
+ }
256
+ if (isVersionRequest(args)) {
257
+ console.log(await readVersion());
258
+ return;
259
+ }
260
+ if (args.length === 0 || isHelpRequest(args)) {
261
+ console.log(HELP_TEXT);
262
+ return;
263
+ }
264
+ const configPath = extractConfigPath(args);
265
+ const fileConfig = configPath ? await loadConfig(configPath) : {};
266
+ const cliConfig = parseCliArgs(args);
267
+ const config = { ...fileConfig, ...cliConfig };
268
+ if (config.outDir && config.outFile) {
269
+ console.error("Error: provide only one of --out-dir or --out-file, not both.");
270
+ process.exit(1);
271
+ }
272
+ if (config.rootType && config.schemaDir) {
273
+ console.error("Error: --root-type cannot be combined with --schema-dir; each schema derives its own root type.");
274
+ process.exit(1);
275
+ }
276
+ if (config.build && config.importExt === "ts") {
277
+ console.error("Error: --import-ext ts cannot be combined with --build. Drop --build to keep runnable .ts sources.");
278
+ process.exit(1);
279
+ }
280
+ if (config.validators && config.typesOnly) {
281
+ console.error("Error: --validators cannot be combined with --types-only, which emits no runtime code.");
282
+ process.exit(1);
283
+ }
284
+ if (config.validators && config.outFile) {
285
+ console.error("Error: --validators cannot be combined with --out-file. Use --out-dir to emit validator files.");
286
+ process.exit(1);
287
+ }
288
+ if (config.outFile) {
289
+ if (!config.typesOnly) {
290
+ console.error("Error: --out-file currently supports only --types-only output. Add --types-only or use --out-dir.");
291
+ process.exit(1);
379
292
  }
380
- const outputDir = resolve(config.outDir);
381
- // schemaDir activates recursive mode and takes precedence over a single schema.
382
293
  if (config.schemaDir) {
383
- await runRecursive(config, resolve(config.schemaDir), outputDir);
384
- return;
294
+ console.error("Error: --out-file cannot be combined with --schema-dir; use --out-dir for recursive output.");
295
+ process.exit(1);
385
296
  }
386
297
  if (!config.schema) {
387
- console.error('Error: --schema (or --schemaDir) is required. Provide a path to a JSON Schema.');
388
- process.exit(1);
389
- }
390
- await runSingle(config, resolve(config.schema), outputDir);
298
+ console.error("Error: --schema is required. Provide a path to a JSON Schema.");
299
+ process.exit(1);
300
+ }
301
+ await runSingleFile(config, resolve(config.schema), resolve(config.outFile));
302
+ return;
303
+ }
304
+ if (!config.outDir) {
305
+ console.error("Error: --out-dir or --out-file is required. Provide an output destination for generated files.");
306
+ process.exit(1);
307
+ }
308
+ const outputDir = resolve(config.outDir);
309
+ if (config.schemaDir) {
310
+ await runRecursive(config, resolve(config.schemaDir), outputDir);
311
+ return;
312
+ }
313
+ if (!config.schema) {
314
+ console.error("Error: --schema (or --schemaDir) is required. Provide a path to a JSON Schema.");
315
+ process.exit(1);
316
+ }
317
+ await runSingle(config, resolve(config.schema), outputDir);
391
318
  };
392
319
  run().catch((error) => {
393
- console.error(error instanceof Error ? error.message : error);
394
- process.exit(1);
320
+ console.error(error instanceof Error ? error.message : error);
321
+ process.exit(1);
395
322
  });