@amritk/mjst 0.2.1 → 0.3.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 +2 -1
- package/dist/cli-config.d.ts +10 -0
- package/dist/cli.js +107 -37
- package/dist/detect-helpers-mode.d.ts +10 -0
- package/package.json +5 -3
- package/src/cli-config.ts +41 -0
- package/src/cli.ts +134 -0
- package/src/detect-helpers-mode.test.ts +45 -0
- package/src/detect-helpers-mode.ts +21 -0
- package/src/load-config.test.ts +69 -0
- package/src/load-config.ts +29 -0
- package/src/parse-cli-args.test.ts +226 -0
- package/src/parse-cli-args.ts +97 -0
package/README.md
CHANGED
|
@@ -69,7 +69,8 @@ npx mjst --config ./mjst.config.json
|
|
|
69
69
|
| 🔨 | `build` | `--build` | `boolean` | — | `false` | Compile the generated TypeScript files to .js and .d.ts output. A temporary tsconfig is written to the output directory, tsc is invoked, and the intermediate .ts source files are removed when compilation succeeds. |
|
|
70
70
|
| ⚠️ | `logWarnings` | `--log-warnings` | `boolean` | — | `false` | Emit a console.warn in the generated parsers for every input key that is not declared in the schema's properties. Useful for detecting schema drift or unexpected data shapes at runtime. |
|
|
71
71
|
| 🚫 | `strict` | `--strict` | `boolean` | — | `false` | Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. Unknown extra keys are still allowed. |
|
|
72
|
-
|
|
|
72
|
+
| 🧰 | `helpers` | `--helpers <mode>` | `string` | — | — | Controls how generated parsers reference their runtime helpers. 'package' emits imports from @amritk/helpers (requires it to be installed in the consumer project). 'embedded' ships the helper source under outDir/_helpers/ so the output is self-contained. When omitted, the CLI auto-detects: it picks 'package' if @amritk/helpers resolves from outDir, otherwise 'embedded'. |
|
|
73
|
+
| ⚙️ | `config` | `--config <path>` | `string` | — | — | Path to a JSON config file. Keys match the option names in this schema (schema, outDir, typesOnly, build, logWarnings, strict, helpers). CLI flags take precedence over config file values. |
|
|
73
74
|
<!-- config-table-end -->
|
|
74
75
|
|
|
75
76
|
---
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -28,4 +28,14 @@ export type CliConfig = {
|
|
|
28
28
|
* instead of coercing invalid input to default values.
|
|
29
29
|
*/
|
|
30
30
|
readonly strict?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Controls how generated parsers reference their runtime helpers.
|
|
33
|
+
* - `'package'`: emit `import ... from '@amritk/helpers/...'`.
|
|
34
|
+
* - `'embedded'`: ship the helper source under `outDir/_helpers/` and emit
|
|
35
|
+
* relative imports so the output directory is self-contained.
|
|
36
|
+
*
|
|
37
|
+
* When omitted, the CLI auto-detects: it picks `'package'` if `@amritk/helpers`
|
|
38
|
+
* resolves from outDir, otherwise falls back to `'embedded'`.
|
|
39
|
+
*/
|
|
40
|
+
readonly helpers?: 'package' | 'embedded';
|
|
31
41
|
};
|
package/dist/cli.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
|
-
import { mkdir, readFile as
|
|
6
|
-
import { dirname, join,
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { mkdir, readFile as readFile3, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname as dirname2, join, resolve as resolve3 } from "node:path";
|
|
8
7
|
import { promisify } from "node:util";
|
|
9
8
|
|
|
10
9
|
// ../generate-parsers/dist/index.js
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { dirname, resolve as resolvePath } from "node:path";
|
|
11
13
|
var isSchemaObject = (schema) => {
|
|
12
14
|
return typeof schema === "object" && schema !== null && typeof schema !== "boolean";
|
|
13
15
|
};
|
|
@@ -783,21 +785,43 @@ var generateTypeDefinition = (schema, typeName) => {
|
|
|
783
785
|
}
|
|
784
786
|
return "export type " + typeName + " = unknown;";
|
|
785
787
|
};
|
|
786
|
-
var
|
|
788
|
+
var PACKAGE_IMPORTS = {
|
|
789
|
+
"is-object": "import { isObject } from '@amritk/helpers/is-object';",
|
|
790
|
+
"validate-array": "import { validateArray } from '@amritk/helpers/validate-array';",
|
|
791
|
+
"validate-record": "import { validateRecord } from '@amritk/helpers/validate-record';",
|
|
792
|
+
"has-ref": "import { hasRef } from '@amritk/helpers/schema-guards';"
|
|
793
|
+
};
|
|
794
|
+
var EMBEDDED_IMPORTS = {
|
|
795
|
+
"is-object": "import { isObject } from './_helpers/is-object';",
|
|
796
|
+
"validate-array": "import { validateArray } from './_helpers/validate-array';",
|
|
797
|
+
"validate-record": "import { validateRecord } from './_helpers/validate-record';",
|
|
798
|
+
"has-ref": "import { hasRef } from './_helpers/has-ref';"
|
|
799
|
+
};
|
|
800
|
+
var collectHelpers = (parserFunction, mode) => {
|
|
787
801
|
const imports = [];
|
|
802
|
+
const used = new Set;
|
|
803
|
+
const table = mode === "embedded" ? EMBEDDED_IMPORTS : PACKAGE_IMPORTS;
|
|
788
804
|
if (parserFunction.includes("validateArray")) {
|
|
789
|
-
imports.push("
|
|
805
|
+
imports.push(table["validate-array"]);
|
|
806
|
+
used.add("validate-array");
|
|
790
807
|
}
|
|
791
808
|
if (parserFunction.includes("validateRecord")) {
|
|
792
|
-
imports.push("
|
|
809
|
+
imports.push(table["validate-record"]);
|
|
810
|
+
used.add("validate-record");
|
|
811
|
+
used.add("is-object");
|
|
793
812
|
}
|
|
794
813
|
if (parserFunction.includes("isObject")) {
|
|
795
|
-
imports.push("
|
|
814
|
+
imports.push(table["is-object"]);
|
|
815
|
+
used.add("is-object");
|
|
796
816
|
}
|
|
797
817
|
if (parserFunction.includes("hasRef(")) {
|
|
798
|
-
imports.push("
|
|
818
|
+
imports.push(table["has-ref"]);
|
|
819
|
+
used.add("has-ref");
|
|
799
820
|
}
|
|
800
|
-
return imports;
|
|
821
|
+
return { imports, used };
|
|
822
|
+
};
|
|
823
|
+
var hasRef = (value) => {
|
|
824
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string";
|
|
801
825
|
};
|
|
802
826
|
var isSchemaObject3 = (schema) => {
|
|
803
827
|
return typeof schema === "object" && schema !== null && typeof schema !== "boolean";
|
|
@@ -880,9 +904,6 @@ var hasMinProperties = (schema) => {
|
|
|
880
904
|
var hasMaxProperties = (schema) => {
|
|
881
905
|
return isSchemaObject3(schema) && "maxProperties" in schema && typeof schema.maxProperties === "number";
|
|
882
906
|
};
|
|
883
|
-
var hasRef = (schema) => {
|
|
884
|
-
return isSchemaObject3(schema) && "$ref" in schema && typeof schema.$ref === "string";
|
|
885
|
-
};
|
|
886
907
|
var getImportPathForFilename = (filename) => `./${filename}`;
|
|
887
908
|
var collectImports = (schema, options) => {
|
|
888
909
|
const typesOnly = options?.typesOnly === true;
|
|
@@ -2379,6 +2400,7 @@ var generateFile = (schema, typeName, options) => {
|
|
|
2379
2400
|
const typesOnly = options?.typesOnly === true;
|
|
2380
2401
|
const selfRef = options?.selfRef;
|
|
2381
2402
|
const rootSchema = options?.rootSchema;
|
|
2403
|
+
const helpersMode = options?.helpersMode ?? "package";
|
|
2382
2404
|
const typeDefinition = generateTypeDefinition(schema, typeName);
|
|
2383
2405
|
if (typesOnly) {
|
|
2384
2406
|
const imports2 = collectImports(schema, { typesOnly: true, selfRef, rootSchema });
|
|
@@ -2393,7 +2415,7 @@ var generateFile = (schema, typeName, options) => {
|
|
|
2393
2415
|
|
|
2394
2416
|
`;
|
|
2395
2417
|
}
|
|
2396
|
-
return result2 + typeDefinition;
|
|
2418
|
+
return { content: result2 + typeDefinition, usedHelpers: new Set };
|
|
2397
2419
|
}
|
|
2398
2420
|
const parserFunction = generateParserFunction(schema, typeName, {
|
|
2399
2421
|
useRefImports: true,
|
|
@@ -2404,7 +2426,8 @@ var generateFile = (schema, typeName, options) => {
|
|
|
2404
2426
|
const combinedFunctions = `${shapeValidator}
|
|
2405
2427
|
|
|
2406
2428
|
${parserFunction}`;
|
|
2407
|
-
const
|
|
2429
|
+
const helpers = collectHelpers(combinedFunctions, helpersMode);
|
|
2430
|
+
const imports = [...collectImports(schema, { selfRef, rootSchema }), ...helpers.imports];
|
|
2408
2431
|
let result = "";
|
|
2409
2432
|
if (imports.length > 0) {
|
|
2410
2433
|
result += imports[0];
|
|
@@ -2416,9 +2439,15 @@ ${parserFunction}`;
|
|
|
2416
2439
|
|
|
2417
2440
|
`;
|
|
2418
2441
|
}
|
|
2419
|
-
return result + typeDefinition + `
|
|
2442
|
+
return { content: result + typeDefinition + `
|
|
2420
2443
|
|
|
2421
|
-
` + combinedFunctions;
|
|
2444
|
+
` + combinedFunctions, usedHelpers: helpers.used };
|
|
2445
|
+
};
|
|
2446
|
+
var readHelperSource = async (helper) => {
|
|
2447
|
+
const require2 = createRequire(import.meta.url);
|
|
2448
|
+
const helpersPkgPath = require2.resolve("@amritk/helpers/package.json");
|
|
2449
|
+
const helpersRoot = dirname(helpersPkgPath);
|
|
2450
|
+
return readFile(resolvePath(helpersRoot, "src", `${helper}.ts`), "utf-8");
|
|
2422
2451
|
};
|
|
2423
2452
|
var extractDynamicAnchorDefs = (schema) => {
|
|
2424
2453
|
const refs = [];
|
|
@@ -2439,18 +2468,20 @@ var extractDynamicAnchorDefs = (schema) => {
|
|
|
2439
2468
|
}
|
|
2440
2469
|
return refs;
|
|
2441
2470
|
};
|
|
2442
|
-
var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict) => {
|
|
2471
|
+
var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package") => {
|
|
2443
2472
|
rootSchema = upgradeDraft07Schema(rootSchema);
|
|
2444
2473
|
const files = [];
|
|
2445
2474
|
const processedRefs = new Set;
|
|
2446
2475
|
const processedFilenames = new Set;
|
|
2447
2476
|
const refsToProcess = [];
|
|
2477
|
+
const usedHelpers = new Set;
|
|
2448
2478
|
const dynamicRefMap = buildDynamicRefMap(rootSchema);
|
|
2449
2479
|
const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap);
|
|
2450
2480
|
const extendedRootSchema = extensions ? applySchemaExtensions(processedRootSchema, rootTypeName.toLowerCase(), extensions) : processedRootSchema;
|
|
2451
|
-
const
|
|
2481
|
+
const rootResult = generateFile(extendedRootSchema, rootTypeName, {
|
|
2452
2482
|
typesOnly: typesOnly ?? false,
|
|
2453
2483
|
rootSchema,
|
|
2484
|
+
helpersMode,
|
|
2454
2485
|
...logWarnings !== undefined ? { logWarnings } : {},
|
|
2455
2486
|
...strict !== undefined ? { strict } : {}
|
|
2456
2487
|
});
|
|
@@ -2458,8 +2489,10 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2458
2489
|
processedFilenames.add(rootFilename);
|
|
2459
2490
|
files.push({
|
|
2460
2491
|
filename: `${rootFilename}.ts`,
|
|
2461
|
-
content:
|
|
2492
|
+
content: rootResult.content
|
|
2462
2493
|
});
|
|
2494
|
+
for (const helper of rootResult.usedHelpers)
|
|
2495
|
+
usedHelpers.add(helper);
|
|
2463
2496
|
const rootRefs = extractRefs(rootSchema);
|
|
2464
2497
|
refsToProcess.push(...rootRefs);
|
|
2465
2498
|
const dynamicAnchorRefs = extractDynamicAnchorDefs(rootSchema);
|
|
@@ -2479,10 +2512,11 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2479
2512
|
const filename = refToFilename(ref);
|
|
2480
2513
|
const processedSchema = resolveDynamicRefs(resolvedSchema, dynamicRefMap);
|
|
2481
2514
|
const extendedSchema = extensions ? applySchemaExtensions(processedSchema, filename, extensions) : processedSchema;
|
|
2482
|
-
const
|
|
2515
|
+
const fileResult = generateFile(extendedSchema, typeName, {
|
|
2483
2516
|
typesOnly: typesOnly ?? false,
|
|
2484
2517
|
selfRef: ref,
|
|
2485
2518
|
rootSchema,
|
|
2519
|
+
helpersMode,
|
|
2486
2520
|
...logWarnings !== undefined ? { logWarnings } : {},
|
|
2487
2521
|
...strict !== undefined ? { strict } : {}
|
|
2488
2522
|
});
|
|
@@ -2490,9 +2524,11 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2490
2524
|
processedFilenames.add(filename);
|
|
2491
2525
|
files.push({
|
|
2492
2526
|
filename: `${filename}.ts`,
|
|
2493
|
-
content
|
|
2527
|
+
content: fileResult.content
|
|
2494
2528
|
});
|
|
2495
2529
|
}
|
|
2530
|
+
for (const helper of fileResult.usedHelpers)
|
|
2531
|
+
usedHelpers.add(helper);
|
|
2496
2532
|
const nestedRefs = extractRefs(resolvedSchema);
|
|
2497
2533
|
for (const nestedRef of nestedRefs) {
|
|
2498
2534
|
if (!processedRefs.has(nestedRef)) {
|
|
@@ -2500,9 +2536,15 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2500
2536
|
}
|
|
2501
2537
|
}
|
|
2502
2538
|
}
|
|
2539
|
+
if (helpersMode === "embedded" && !typesOnly) {
|
|
2540
|
+
for (const helper of usedHelpers) {
|
|
2541
|
+
const source = await readHelperSource(helper);
|
|
2542
|
+
files.push({ filename: `_helpers/${helper}.ts`, content: source });
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2503
2545
|
const TYPE_EXPORT_RE = /^export type (\w+)/gm;
|
|
2504
2546
|
const CONST_EXPORT_RE = /^export const (\w+)/gm;
|
|
2505
|
-
const sortedFiles = [...files].sort((a, b) => a.filename.localeCompare(b.filename));
|
|
2547
|
+
const sortedFiles = [...files].filter((f) => !f.filename.startsWith("_helpers/")).sort((a, b) => a.filename.localeCompare(b.filename));
|
|
2506
2548
|
let indexContent = "";
|
|
2507
2549
|
for (const file of sortedFiles) {
|
|
2508
2550
|
const moduleName = file.filename.replace(/\.ts$/, "");
|
|
@@ -2533,12 +2575,25 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
|
|
|
2533
2575
|
return files;
|
|
2534
2576
|
};
|
|
2535
2577
|
|
|
2536
|
-
// src/
|
|
2537
|
-
import {
|
|
2578
|
+
// src/detect-helpers-mode.ts
|
|
2579
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
2538
2580
|
import { resolve } from "node:path";
|
|
2581
|
+
var detectHelpersMode = (outputDir) => {
|
|
2582
|
+
const req = createRequire2(resolve(outputDir, "noop.js"));
|
|
2583
|
+
try {
|
|
2584
|
+
req.resolve("@amritk/helpers/is-object");
|
|
2585
|
+
return "package";
|
|
2586
|
+
} catch {
|
|
2587
|
+
return "embedded";
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
|
|
2591
|
+
// src/load-config.ts
|
|
2592
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
2593
|
+
import { resolve as resolve2 } from "node:path";
|
|
2539
2594
|
var loadConfig = async (configPath) => {
|
|
2540
|
-
const absolutePath =
|
|
2541
|
-
const raw = await
|
|
2595
|
+
const absolutePath = resolve2(configPath);
|
|
2596
|
+
const raw = await readFile2(absolutePath, "utf-8");
|
|
2542
2597
|
const parsed = JSON.parse(raw);
|
|
2543
2598
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2544
2599
|
throw new Error(`Config file must be a JSON object: ${configPath}`);
|
|
@@ -2557,6 +2612,11 @@ var loadConfig = async (configPath) => {
|
|
|
2557
2612
|
// src/parse-cli-args.ts
|
|
2558
2613
|
var parseCliArgs = (args) => {
|
|
2559
2614
|
const config = {};
|
|
2615
|
+
const parseHelpersValue = (value) => {
|
|
2616
|
+
if (value === "package" || value === "embedded")
|
|
2617
|
+
return value;
|
|
2618
|
+
return;
|
|
2619
|
+
};
|
|
2560
2620
|
for (let i = 0;i < args.length; i++) {
|
|
2561
2621
|
const arg = args[i];
|
|
2562
2622
|
if (!arg) {
|
|
@@ -2578,6 +2638,10 @@ var parseCliArgs = (args) => {
|
|
|
2578
2638
|
config.logWarnings = value !== "false";
|
|
2579
2639
|
} else if (key === "strict") {
|
|
2580
2640
|
config.strict = value !== "false";
|
|
2641
|
+
} else if (key === "helpers") {
|
|
2642
|
+
const parsed = parseHelpersValue(value);
|
|
2643
|
+
if (parsed)
|
|
2644
|
+
config.helpers = parsed;
|
|
2581
2645
|
}
|
|
2582
2646
|
continue;
|
|
2583
2647
|
}
|
|
@@ -2601,6 +2665,15 @@ var parseCliArgs = (args) => {
|
|
|
2601
2665
|
config.logWarnings = true;
|
|
2602
2666
|
} else if (arg === "--strict") {
|
|
2603
2667
|
config.strict = true;
|
|
2668
|
+
} else if (arg === "--helpers") {
|
|
2669
|
+
const value = args[i + 1];
|
|
2670
|
+
if (value && !value.startsWith("--")) {
|
|
2671
|
+
const parsed = parseHelpersValue(value);
|
|
2672
|
+
if (parsed) {
|
|
2673
|
+
config.helpers = parsed;
|
|
2674
|
+
i++;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2604
2677
|
}
|
|
2605
2678
|
}
|
|
2606
2679
|
return config;
|
|
@@ -2634,21 +2707,21 @@ var run = async () => {
|
|
|
2634
2707
|
console.error("Error: --outDir is required. Provide an output directory for generated files.");
|
|
2635
2708
|
process.exit(1);
|
|
2636
2709
|
}
|
|
2637
|
-
const schemaPath =
|
|
2638
|
-
const raw = await
|
|
2710
|
+
const schemaPath = resolve3(config.schema);
|
|
2711
|
+
const raw = await readFile3(schemaPath, "utf-8");
|
|
2639
2712
|
const schema = JSON.parse(raw);
|
|
2640
|
-
const
|
|
2641
|
-
const outputDir = resolve2(config.outDir);
|
|
2713
|
+
const outputDir = resolve3(config.outDir);
|
|
2642
2714
|
await mkdir(outputDir, { recursive: true });
|
|
2715
|
+
const helpersMode = config.helpers ?? detectHelpersMode(outputDir);
|
|
2716
|
+
console.log(`Helpers mode: ${helpersMode}${config.helpers ? " (explicit)" : " (auto-detected)"}`);
|
|
2717
|
+
const files = await buildSchema(schema, "Document", undefined, config.typesOnly, config.logWarnings, config.strict, helpersMode);
|
|
2643
2718
|
for (const file of files) {
|
|
2644
2719
|
const filePath = join(outputDir, file.filename);
|
|
2645
|
-
await mkdir(
|
|
2720
|
+
await mkdir(dirname2(filePath), { recursive: true });
|
|
2646
2721
|
await writeFile(filePath, file.content, "utf-8");
|
|
2647
2722
|
console.log(`Generated: ${file.filename}`);
|
|
2648
2723
|
}
|
|
2649
2724
|
if (config.build) {
|
|
2650
|
-
const helpersDir = resolve2(dirname(fileURLToPath(import.meta.url)), "../../helpers/dist");
|
|
2651
|
-
const helpersRelative = relative(outputDir, helpersDir);
|
|
2652
2725
|
const tsconfigContent = JSON.stringify({
|
|
2653
2726
|
compilerOptions: {
|
|
2654
2727
|
target: "ESNext",
|
|
@@ -2656,10 +2729,7 @@ var run = async () => {
|
|
|
2656
2729
|
moduleResolution: "bundler",
|
|
2657
2730
|
declaration: true,
|
|
2658
2731
|
emitDeclarationOnly: config.typesOnly,
|
|
2659
|
-
skipLibCheck: true
|
|
2660
|
-
paths: {
|
|
2661
|
-
"@amritk/helpers/*": [`${helpersRelative}/*`]
|
|
2662
|
-
}
|
|
2732
|
+
skipLibCheck: true
|
|
2663
2733
|
},
|
|
2664
2734
|
include: ["./**/*.ts"]
|
|
2665
2735
|
}, null, 2);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects whether `@amritk/helpers` is resolvable from the consumer's output
|
|
3
|
+
* directory using standard Node module resolution (walks up `node_modules`).
|
|
4
|
+
*
|
|
5
|
+
* Returns `'package'` when generated parsers can safely
|
|
6
|
+
* `import { ... } from '@amritk/helpers/...'` at runtime, or `'embedded'`
|
|
7
|
+
* when the helper sources need to be shipped alongside the generated files
|
|
8
|
+
* for the output to be self-contained.
|
|
9
|
+
*/
|
|
10
|
+
export declare const detectHelpersMode: (outputDir: string) => "package" | "embedded";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/mjst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"mjst": "./dist/cli.js"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"dist"
|
|
30
|
+
"dist",
|
|
31
|
+
"src"
|
|
31
32
|
],
|
|
32
33
|
"publishConfig": {
|
|
33
34
|
"access": "public"
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"@amritk/generate-parsers": "workspace:*",
|
|
45
|
-
"@amritk/generate-markdown": "workspace:*"
|
|
46
|
+
"@amritk/generate-markdown": "workspace:*",
|
|
47
|
+
"@amritk/helpers": "workspace:*"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"json-schema-typed": "catalog:"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the CLI tool.
|
|
3
|
+
* These properties map 1:1 with CLI flags and config file keys.
|
|
4
|
+
*/
|
|
5
|
+
export type CliConfig = {
|
|
6
|
+
/** Path to the JSON Schema file to process. */
|
|
7
|
+
readonly schema: string
|
|
8
|
+
/** Output directory for generated TypeScript files. */
|
|
9
|
+
readonly outDir: string
|
|
10
|
+
/**
|
|
11
|
+
* When true, only generate TypeScript type definitions without parser functions.
|
|
12
|
+
* Useful when you only need the type shapes and do not need runtime validation.
|
|
13
|
+
*/
|
|
14
|
+
readonly typesOnly?: boolean
|
|
15
|
+
/**
|
|
16
|
+
* When true, compile the generated TypeScript files to .js and .d.ts output.
|
|
17
|
+
* The .ts source files are removed after compilation.
|
|
18
|
+
*/
|
|
19
|
+
readonly build?: boolean
|
|
20
|
+
/**
|
|
21
|
+
* When true, the generated parsers emit a console.warn for every input key
|
|
22
|
+
* that is not declared in the schema's properties.
|
|
23
|
+
*/
|
|
24
|
+
readonly logWarnings?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* When true, the generated parsers throw on type/shape mismatches
|
|
27
|
+
* (wrong type, missing required property, enum/pattern/min/max violations)
|
|
28
|
+
* instead of coercing invalid input to default values.
|
|
29
|
+
*/
|
|
30
|
+
readonly strict?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Controls how generated parsers reference their runtime helpers.
|
|
33
|
+
* - `'package'`: emit `import ... from '@amritk/helpers/...'`.
|
|
34
|
+
* - `'embedded'`: ship the helper source under `outDir/_helpers/` and emit
|
|
35
|
+
* relative imports so the output directory is self-contained.
|
|
36
|
+
*
|
|
37
|
+
* When omitted, the CLI auto-detects: it picks `'package'` if `@amritk/helpers`
|
|
38
|
+
* resolves from outDir, otherwise falls back to `'embedded'`.
|
|
39
|
+
*/
|
|
40
|
+
readonly helpers?: 'package' | 'embedded'
|
|
41
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join, resolve } from 'node:path'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
import { buildSchema } from '@amritk/generate-parsers'
|
|
7
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile)
|
|
10
|
+
|
|
11
|
+
import { detectHelpersMode } from './detect-helpers-mode'
|
|
12
|
+
import { loadConfig } from './load-config'
|
|
13
|
+
import { parseCliArgs } from './parse-cli-args'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extracts the --config flag value from process args before full parsing.
|
|
17
|
+
* We need this early so we can load the config file first, then overlay CLI flags.
|
|
18
|
+
*/
|
|
19
|
+
const extractConfigPath = (args: readonly string[]): string | undefined => {
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
const arg = args[i]
|
|
22
|
+
|
|
23
|
+
if (arg === '--config' && args[i + 1] && !args[i + 1]?.startsWith('--')) {
|
|
24
|
+
return args[i + 1]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (arg?.startsWith('--config=')) {
|
|
28
|
+
return arg.slice('--config='.length)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const run = async (): Promise<void> => {
|
|
36
|
+
// Skip the first two args (node executable and script path)
|
|
37
|
+
const args = process.argv.slice(2)
|
|
38
|
+
|
|
39
|
+
const configPath = extractConfigPath(args)
|
|
40
|
+
|
|
41
|
+
// Start with config file values if provided, then overlay CLI flags on top
|
|
42
|
+
const fileConfig = configPath ? await loadConfig(configPath) : {}
|
|
43
|
+
const cliConfig = parseCliArgs(args)
|
|
44
|
+
const config = { ...fileConfig, ...cliConfig }
|
|
45
|
+
|
|
46
|
+
if (!config.schema) {
|
|
47
|
+
console.error('Error: --schema is required. Provide a path to a JSON Schema file.')
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!config.outDir) {
|
|
52
|
+
console.error('Error: --outDir is required. Provide an output directory for generated files.')
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const schemaPath = resolve(config.schema)
|
|
57
|
+
const raw = await readFile(schemaPath, 'utf-8')
|
|
58
|
+
const schema: unknown = JSON.parse(raw)
|
|
59
|
+
|
|
60
|
+
const outputDir = resolve(config.outDir)
|
|
61
|
+
await mkdir(outputDir, { recursive: true })
|
|
62
|
+
|
|
63
|
+
const helpersMode = config.helpers ?? detectHelpersMode(outputDir)
|
|
64
|
+
console.log(`Helpers mode: ${helpersMode}${config.helpers ? ' (explicit)' : ' (auto-detected)'}`)
|
|
65
|
+
|
|
66
|
+
const files = await buildSchema(
|
|
67
|
+
schema as JSONSchema,
|
|
68
|
+
'Document',
|
|
69
|
+
undefined,
|
|
70
|
+
config.typesOnly,
|
|
71
|
+
config.logWarnings,
|
|
72
|
+
config.strict,
|
|
73
|
+
helpersMode,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const filePath = join(outputDir, file.filename)
|
|
78
|
+
// Create subdirectories if the filename contains a path
|
|
79
|
+
await mkdir(dirname(filePath), { recursive: true })
|
|
80
|
+
await writeFile(filePath, file.content, 'utf-8')
|
|
81
|
+
console.log(`Generated: ${file.filename}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (config.build) {
|
|
85
|
+
// Write a minimal tsconfig so tsc can compile the generated files without
|
|
86
|
+
// inheriting settings like allowImportingTsExtensions that block emission.
|
|
87
|
+
// In 'package' mode tsc finds @amritk/helpers via the consumer's node_modules.
|
|
88
|
+
// In 'embedded' mode the helpers live under ./_helpers and need no resolution.
|
|
89
|
+
const tsconfigContent = JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
compilerOptions: {
|
|
92
|
+
target: 'ESNext',
|
|
93
|
+
module: 'ESNext',
|
|
94
|
+
moduleResolution: 'bundler',
|
|
95
|
+
declaration: true,
|
|
96
|
+
emitDeclarationOnly: config.typesOnly,
|
|
97
|
+
skipLibCheck: true,
|
|
98
|
+
},
|
|
99
|
+
include: ['./**/*.ts'],
|
|
100
|
+
},
|
|
101
|
+
null,
|
|
102
|
+
2,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const tsconfigPath = join(outputDir, '.tsconfig-mjst-build.json')
|
|
106
|
+
await writeFile(tsconfigPath, tsconfigContent, 'utf-8')
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await execFileAsync('npx', ['tsc', '--project', tsconfigPath])
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error('TypeScript compilation failed. Check the generated files for errors.')
|
|
112
|
+
} finally {
|
|
113
|
+
await unlink(tsconfigPath)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Remove the intermediate .ts files now that .js and .d.ts have been produced
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
await unlink(join(outputDir, file.filename))
|
|
119
|
+
const jsFilename = file.filename.replace(/\.ts$/, '.js')
|
|
120
|
+
const dtsFilename = file.filename.replace(/\.ts$/, '.d.ts')
|
|
121
|
+
console.log(`Built: ${jsFilename}`)
|
|
122
|
+
console.log(`Built: ${dtsFilename}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`\nTotal files built: ${files.length * 2}`)
|
|
126
|
+
} else {
|
|
127
|
+
console.log(`\nTotal files generated: ${files.length}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
run().catch((error: unknown) => {
|
|
132
|
+
console.error(error instanceof Error ? error.message : error)
|
|
133
|
+
process.exit(1)
|
|
134
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { detectHelpersMode } from './detect-helpers-mode'
|
|
7
|
+
|
|
8
|
+
describe('detect-helpers-mode', () => {
|
|
9
|
+
let tmpRoot: string
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
tmpRoot = await mkdtemp(join(tmpdir(), 'mjst-detect-'))
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await rm(tmpRoot, { recursive: true, force: true })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("returns 'package' when @amritk/helpers is resolvable from outDir", async () => {
|
|
20
|
+
const outDir = join(tmpRoot, 'with-helpers/out')
|
|
21
|
+
const helpersPkgDir = join(tmpRoot, 'with-helpers/node_modules/@amritk/helpers')
|
|
22
|
+
await mkdir(outDir, { recursive: true })
|
|
23
|
+
await mkdir(helpersPkgDir, { recursive: true })
|
|
24
|
+
await writeFile(join(helpersPkgDir, 'is-object.js'), 'export const isObject = () => true\n', 'utf-8')
|
|
25
|
+
await writeFile(
|
|
26
|
+
join(helpersPkgDir, 'package.json'),
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
name: '@amritk/helpers',
|
|
29
|
+
version: '0.0.0',
|
|
30
|
+
type: 'module',
|
|
31
|
+
exports: { './is-object': './is-object.js' },
|
|
32
|
+
}),
|
|
33
|
+
'utf-8',
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(detectHelpersMode(outDir)).toBe('package')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("returns 'embedded' when @amritk/helpers cannot be resolved from outDir", async () => {
|
|
40
|
+
const outDir = join(tmpRoot, 'no-helpers/out')
|
|
41
|
+
await mkdir(outDir, { recursive: true })
|
|
42
|
+
|
|
43
|
+
expect(detectHelpersMode(outDir)).toBe('embedded')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detects whether `@amritk/helpers` is resolvable from the consumer's output
|
|
6
|
+
* directory using standard Node module resolution (walks up `node_modules`).
|
|
7
|
+
*
|
|
8
|
+
* Returns `'package'` when generated parsers can safely
|
|
9
|
+
* `import { ... } from '@amritk/helpers/...'` at runtime, or `'embedded'`
|
|
10
|
+
* when the helper sources need to be shipped alongside the generated files
|
|
11
|
+
* for the output to be self-contained.
|
|
12
|
+
*/
|
|
13
|
+
export const detectHelpersMode = (outputDir: string): 'package' | 'embedded' => {
|
|
14
|
+
const req = createRequire(resolve(outputDir, 'noop.js'))
|
|
15
|
+
try {
|
|
16
|
+
req.resolve('@amritk/helpers/is-object')
|
|
17
|
+
return 'package'
|
|
18
|
+
} catch {
|
|
19
|
+
return 'embedded'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { loadConfig } from './load-config'
|
|
7
|
+
|
|
8
|
+
describe('load-config', () => {
|
|
9
|
+
it('loads schema and outDir from a JSON config file', async () => {
|
|
10
|
+
const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
|
|
11
|
+
await writeFile(configPath, JSON.stringify({ schema: 'my-schema.json', outDir: 'output' }))
|
|
12
|
+
|
|
13
|
+
const result = await loadConfig(configPath)
|
|
14
|
+
|
|
15
|
+
expect(result).toEqual({
|
|
16
|
+
schema: 'my-schema.json',
|
|
17
|
+
outDir: 'output',
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns partial config when only some keys are present', async () => {
|
|
22
|
+
const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
|
|
23
|
+
await writeFile(configPath, JSON.stringify({ schema: 'my-schema.json' }))
|
|
24
|
+
|
|
25
|
+
const result = await loadConfig(configPath)
|
|
26
|
+
|
|
27
|
+
expect(result).toEqual({
|
|
28
|
+
schema: 'my-schema.json',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('ignores non-string values for known keys', async () => {
|
|
33
|
+
const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
|
|
34
|
+
await writeFile(configPath, JSON.stringify({ schema: 123, outDir: true }))
|
|
35
|
+
|
|
36
|
+
const result = await loadConfig(configPath)
|
|
37
|
+
|
|
38
|
+
expect(result).toEqual({})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('throws for non-object config files', async () => {
|
|
42
|
+
const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
|
|
43
|
+
await writeFile(configPath, JSON.stringify([1, 2, 3]))
|
|
44
|
+
|
|
45
|
+
await expect(loadConfig(configPath)).rejects.toThrow('Config file must be a JSON object')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('throws for missing config files', async () => {
|
|
49
|
+
await expect(loadConfig('/nonexistent/config.json')).rejects.toThrow()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('loads strict boolean from config file', async () => {
|
|
53
|
+
const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
|
|
54
|
+
await writeFile(configPath, JSON.stringify({ schema: 's.json', outDir: 'o', strict: true }))
|
|
55
|
+
|
|
56
|
+
const result = await loadConfig(configPath)
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual({ schema: 's.json', outDir: 'o', strict: true })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('ignores non-boolean strict value', async () => {
|
|
62
|
+
const configPath = join(tmpdir(), `test-config-${Date.now()}.json`)
|
|
63
|
+
await writeFile(configPath, JSON.stringify({ schema: 's.json', strict: 'yes' }))
|
|
64
|
+
|
|
65
|
+
const result = await loadConfig(configPath)
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual({ schema: 's.json' })
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { CliConfig } from './cli-config'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Loads a JSON config file and returns the relevant CLI config properties.
|
|
8
|
+
* The config file should have the same keys as the CLI flags (schema, outDir).
|
|
9
|
+
*/
|
|
10
|
+
export const loadConfig = async (configPath: string): Promise<Partial<CliConfig>> => {
|
|
11
|
+
const absolutePath = resolve(configPath)
|
|
12
|
+
const raw = await readFile(absolutePath, 'utf-8')
|
|
13
|
+
const parsed: unknown = JSON.parse(raw)
|
|
14
|
+
|
|
15
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
16
|
+
throw new Error(`Config file must be a JSON object: ${configPath}`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const obj = parsed as Record<string, unknown>
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...(typeof obj['schema'] === 'string' && { schema: obj['schema'] }),
|
|
23
|
+
...(typeof obj['outDir'] === 'string' && { outDir: obj['outDir'] }),
|
|
24
|
+
...(typeof obj['typesOnly'] === 'boolean' && { typesOnly: obj['typesOnly'] }),
|
|
25
|
+
...(typeof obj['build'] === 'boolean' && { build: obj['build'] }),
|
|
26
|
+
...(typeof obj['logWarnings'] === 'boolean' && { logWarnings: obj['logWarnings'] }),
|
|
27
|
+
...(typeof obj['strict'] === 'boolean' && { strict: obj['strict'] }),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { parseCliArgs } from './parse-cli-args'
|
|
4
|
+
|
|
5
|
+
describe('parse-cli-args', () => {
|
|
6
|
+
it('parses --schema and --outDir with space-separated values', () => {
|
|
7
|
+
const result = parseCliArgs(['--schema', 'path/to/schema.json', '--outDir', 'dist'])
|
|
8
|
+
|
|
9
|
+
expect(result).toEqual({
|
|
10
|
+
schema: 'path/to/schema.json',
|
|
11
|
+
outDir: 'dist',
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('parses --schema and --outDir with equals syntax', () => {
|
|
16
|
+
const result = parseCliArgs(['--schema=path/to/schema.json', '--outDir=dist'])
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual({
|
|
19
|
+
schema: 'path/to/schema.json',
|
|
20
|
+
outDir: 'dist',
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns empty object when no flags are provided', () => {
|
|
25
|
+
const result = parseCliArgs([])
|
|
26
|
+
|
|
27
|
+
expect(result).toEqual({})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('ignores unknown flags', () => {
|
|
31
|
+
const result = parseCliArgs(['--unknown', 'value', '--schema', 'schema.json'])
|
|
32
|
+
|
|
33
|
+
expect(result).toEqual({
|
|
34
|
+
schema: 'schema.json',
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('handles only --schema without --outDir', () => {
|
|
39
|
+
const result = parseCliArgs(['--schema', 'schema.json'])
|
|
40
|
+
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
schema: 'schema.json',
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('handles only --outDir without --schema', () => {
|
|
47
|
+
const result = parseCliArgs(['--outDir', 'output'])
|
|
48
|
+
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
outDir: 'output',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('does not treat a flag as a value for the previous flag', () => {
|
|
55
|
+
const result = parseCliArgs(['--schema', '--outDir', 'output'])
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual({
|
|
58
|
+
outDir: 'output',
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('handles equals syntax with empty value', () => {
|
|
63
|
+
const result = parseCliArgs(['--schema=', '--outDir=dist'])
|
|
64
|
+
|
|
65
|
+
expect(result).toEqual({
|
|
66
|
+
schema: '',
|
|
67
|
+
outDir: 'dist',
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('parses --types-only boolean flag', () => {
|
|
72
|
+
const result = parseCliArgs(['--types-only'])
|
|
73
|
+
|
|
74
|
+
expect(result).toEqual({
|
|
75
|
+
typesOnly: true,
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('parses --types-only=true with equals syntax', () => {
|
|
80
|
+
const result = parseCliArgs(['--types-only=true'])
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
typesOnly: true,
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('parses --types-only=false with equals syntax', () => {
|
|
88
|
+
const result = parseCliArgs(['--types-only=false'])
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual({
|
|
91
|
+
typesOnly: false,
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('parses --types-only alongside other flags', () => {
|
|
96
|
+
const result = parseCliArgs(['--schema', 'schema.json', '--outDir', 'dist', '--types-only'])
|
|
97
|
+
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
schema: 'schema.json',
|
|
100
|
+
outDir: 'dist',
|
|
101
|
+
typesOnly: true,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('parses --build boolean flag', () => {
|
|
106
|
+
const result = parseCliArgs(['--build'])
|
|
107
|
+
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
build: true,
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('parses --build=true with equals syntax', () => {
|
|
114
|
+
const result = parseCliArgs(['--build=true'])
|
|
115
|
+
|
|
116
|
+
expect(result).toEqual({
|
|
117
|
+
build: true,
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('parses --build=false with equals syntax', () => {
|
|
122
|
+
const result = parseCliArgs(['--build=false'])
|
|
123
|
+
|
|
124
|
+
expect(result).toEqual({
|
|
125
|
+
build: false,
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('parses --build alongside other flags', () => {
|
|
130
|
+
const result = parseCliArgs(['--schema', 'schema.json', '--outDir', 'dist', '--build'])
|
|
131
|
+
|
|
132
|
+
expect(result).toEqual({
|
|
133
|
+
schema: 'schema.json',
|
|
134
|
+
outDir: 'dist',
|
|
135
|
+
build: true,
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('parses --log-warnings boolean flag', () => {
|
|
140
|
+
const result = parseCliArgs(['--log-warnings'])
|
|
141
|
+
|
|
142
|
+
expect(result).toEqual({
|
|
143
|
+
logWarnings: true,
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('parses --log-warnings=true with equals syntax', () => {
|
|
148
|
+
const result = parseCliArgs(['--log-warnings=true'])
|
|
149
|
+
|
|
150
|
+
expect(result).toEqual({
|
|
151
|
+
logWarnings: true,
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('parses --log-warnings=false with equals syntax', () => {
|
|
156
|
+
const result = parseCliArgs(['--log-warnings=false'])
|
|
157
|
+
|
|
158
|
+
expect(result).toEqual({
|
|
159
|
+
logWarnings: false,
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('parses --log-warnings alongside other flags', () => {
|
|
164
|
+
const result = parseCliArgs(['--schema', 'schema.json', '--outDir', 'dist', '--log-warnings'])
|
|
165
|
+
|
|
166
|
+
expect(result).toEqual({
|
|
167
|
+
schema: 'schema.json',
|
|
168
|
+
outDir: 'dist',
|
|
169
|
+
logWarnings: true,
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('parses --strict boolean flag', () => {
|
|
174
|
+
const result = parseCliArgs(['--strict'])
|
|
175
|
+
|
|
176
|
+
expect(result).toEqual({
|
|
177
|
+
strict: true,
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('parses --strict=true with equals syntax', () => {
|
|
182
|
+
const result = parseCliArgs(['--strict=true'])
|
|
183
|
+
|
|
184
|
+
expect(result).toEqual({
|
|
185
|
+
strict: true,
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('parses --strict=false with equals syntax', () => {
|
|
190
|
+
const result = parseCliArgs(['--strict=false'])
|
|
191
|
+
|
|
192
|
+
expect(result).toEqual({
|
|
193
|
+
strict: false,
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('parses --strict alongside other flags', () => {
|
|
198
|
+
const result = parseCliArgs(['--schema', 'schema.json', '--outDir', 'dist', '--strict'])
|
|
199
|
+
|
|
200
|
+
expect(result).toEqual({
|
|
201
|
+
schema: 'schema.json',
|
|
202
|
+
outDir: 'dist',
|
|
203
|
+
strict: true,
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('parses --helpers package with space-separated value', () => {
|
|
208
|
+
const result = parseCliArgs(['--helpers', 'package'])
|
|
209
|
+
expect(result).toEqual({ helpers: 'package' })
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('parses --helpers=embedded with equals syntax', () => {
|
|
213
|
+
const result = parseCliArgs(['--helpers=embedded'])
|
|
214
|
+
expect(result).toEqual({ helpers: 'embedded' })
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('ignores invalid --helpers values', () => {
|
|
218
|
+
const result = parseCliArgs(['--helpers', 'bogus', '--schema', 's.json'])
|
|
219
|
+
expect(result).toEqual({ schema: 's.json' })
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('ignores invalid --helpers=foo values', () => {
|
|
223
|
+
const result = parseCliArgs(['--helpers=foo', '--schema', 's.json'])
|
|
224
|
+
expect(result).toEqual({ schema: 's.json' })
|
|
225
|
+
})
|
|
226
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CliConfig } from './cli-config'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses command-line arguments into a partial CLI config.
|
|
5
|
+
* Supports --schema, --outDir, and --types-only flags with either `--flag value` or `--flag=value` syntax.
|
|
6
|
+
* Only returns the flags that were explicitly provided so we can layer them on top of config file values.
|
|
7
|
+
*/
|
|
8
|
+
export const parseCliArgs = (args: readonly string[]): Partial<CliConfig> => {
|
|
9
|
+
// Use a mutable local type for building, then return as Partial<CliConfig>
|
|
10
|
+
const config: {
|
|
11
|
+
schema?: string
|
|
12
|
+
outDir?: string
|
|
13
|
+
typesOnly?: boolean
|
|
14
|
+
build?: boolean
|
|
15
|
+
logWarnings?: boolean
|
|
16
|
+
strict?: boolean
|
|
17
|
+
helpers?: 'package' | 'embedded'
|
|
18
|
+
} = {}
|
|
19
|
+
|
|
20
|
+
const parseHelpersValue = (value: string): 'package' | 'embedded' | undefined => {
|
|
21
|
+
if (value === 'package' || value === 'embedded') return value
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i]
|
|
27
|
+
|
|
28
|
+
if (!arg) {
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle --flag=value syntax
|
|
33
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
34
|
+
const equalsIndex = arg.indexOf('=')
|
|
35
|
+
const key = arg.slice(2, equalsIndex)
|
|
36
|
+
const value = arg.slice(equalsIndex + 1)
|
|
37
|
+
|
|
38
|
+
if (key === 'schema') {
|
|
39
|
+
config.schema = value
|
|
40
|
+
} else if (key === 'outDir') {
|
|
41
|
+
config.outDir = value
|
|
42
|
+
} else if (key === 'types-only') {
|
|
43
|
+
// Accept --types-only=false to explicitly opt out, otherwise treat as true
|
|
44
|
+
config.typesOnly = value !== 'false'
|
|
45
|
+
} else if (key === 'build') {
|
|
46
|
+
config.build = value !== 'false'
|
|
47
|
+
} else if (key === 'log-warnings') {
|
|
48
|
+
config.logWarnings = value !== 'false'
|
|
49
|
+
} else if (key === 'strict') {
|
|
50
|
+
config.strict = value !== 'false'
|
|
51
|
+
} else if (key === 'helpers') {
|
|
52
|
+
const parsed = parseHelpersValue(value)
|
|
53
|
+
if (parsed) config.helpers = parsed
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Handle --flag value syntax
|
|
60
|
+
if (arg === '--schema') {
|
|
61
|
+
const value = args[i + 1]
|
|
62
|
+
|
|
63
|
+
if (value && !value.startsWith('--')) {
|
|
64
|
+
config.schema = value
|
|
65
|
+
i++
|
|
66
|
+
}
|
|
67
|
+
} else if (arg === '--outDir') {
|
|
68
|
+
const value = args[i + 1]
|
|
69
|
+
|
|
70
|
+
if (value && !value.startsWith('--')) {
|
|
71
|
+
config.outDir = value
|
|
72
|
+
i++
|
|
73
|
+
}
|
|
74
|
+
} else if (arg === '--types-only') {
|
|
75
|
+
// Boolean flag: presence alone enables it, no value needed
|
|
76
|
+
config.typesOnly = true
|
|
77
|
+
} else if (arg === '--build') {
|
|
78
|
+
config.build = true
|
|
79
|
+
} else if (arg === '--log-warnings') {
|
|
80
|
+
config.logWarnings = true
|
|
81
|
+
} else if (arg === '--strict') {
|
|
82
|
+
config.strict = true
|
|
83
|
+
} else if (arg === '--helpers') {
|
|
84
|
+
const value = args[i + 1]
|
|
85
|
+
|
|
86
|
+
if (value && !value.startsWith('--')) {
|
|
87
|
+
const parsed = parseHelpersValue(value)
|
|
88
|
+
if (parsed) {
|
|
89
|
+
config.helpers = parsed
|
|
90
|
+
i++
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return config
|
|
97
|
+
}
|