@amritk/generate-parsers 0.3.1 → 0.5.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.
@@ -34,6 +34,12 @@ export type GeneratedFile = {
34
34
  * @param helpersMode - `'package'` (default) emits `import ... from '@amritk/helpers/...'`.
35
35
  * `'embedded'` emits `import ... from './_helpers/...'` and appends the helper sources
36
36
  * as additional `GeneratedFile` entries so the output directory is self-contained.
37
+ * @param helpersImportPrefix - Relative path prefix to the `_helpers/` directory in
38
+ * embedded mode. Defaults to `'./'`. The recursive multi-schema build passes `'../'`,
39
+ * `'../../'`, etc. so nested parsers can import from a single shared `_helpers/`
40
+ * directory while the helper sources are emitted once at the output root.
41
+ * @param readonly - When true, every property, array, and record in the generated type
42
+ * definitions is emitted as `readonly`.
37
43
  * @returns An array of generated TypeScript files
38
44
  *
39
45
  * @example
@@ -66,4 +72,4 @@ export type GeneratedFile = {
66
72
  * });
67
73
  * ```
68
74
  */
69
- export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode) => Promise<GeneratedFile[]>;
75
+ export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean) => Promise<GeneratedFile[]>;
@@ -37,6 +37,18 @@ type GenerateFileOptions = {
37
37
  * be shipped alongside the generated files.
38
38
  */
39
39
  readonly helpersMode?: HelpersMode;
40
+ /**
41
+ * Relative path prefix to the shared `_helpers/` directory in embedded mode.
42
+ * Defaults to `'./'`. The recursive multi-schema build passes `'../'`,
43
+ * `'../../'`, etc. so a nested parser can reach a single `_helpers/` directory
44
+ * at the output root.
45
+ */
46
+ readonly helpersImportPrefix?: string;
47
+ /**
48
+ * When true, every property, array, and record in the generated type
49
+ * definitions is emitted as `readonly`.
50
+ */
51
+ readonly readonly?: boolean;
40
52
  };
41
53
  /** Result of generating a single parser file. */
42
54
  export type GeneratedFileContent = {
@@ -12,5 +12,12 @@ export type CollectedHelpers = {
12
12
  */
13
13
  readonly used: Set<RuntimeHelperName>;
14
14
  };
15
- /** Detects which runtime helpers a generated parser body references. */
16
- export declare const collectHelpers: (parserFunction: string, mode: HelpersMode) => CollectedHelpers;
15
+ /**
16
+ * Detects which runtime helpers a generated parser body references.
17
+ *
18
+ * @param helpersImportPrefix - Relative path prefix to the shared `_helpers/`
19
+ * directory in embedded mode. Defaults to `'./'`. The recursive multi-schema
20
+ * build passes `'../'`, `'../../'`, etc. so nested parsers can reach a single
21
+ * `_helpers/` directory at the output root.
22
+ */
23
+ export declare const collectHelpers: (parserFunction: string, mode: HelpersMode, helpersImportPrefix?: string) => CollectedHelpers;
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { buildSchema } from './generators/build-schema';
1
+ export { buildSchema, type GeneratedFile } from './generators/build-schema';
package/dist/index.js CHANGED
@@ -375,6 +375,31 @@ var isSchemaObject2 = (schema) => {
375
375
  var isObjectSchema = (schema) => {
376
376
  return isSchemaObject2(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema));
377
377
  };
378
+ var MJST_EXTENSION_KEY = "x-mjst";
379
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
380
+ var SUPPORTED_PRIMITIVES = new Set(["bigint"]);
381
+ var SAFE_BRAND = /^[\w$ -]+$/;
382
+ var readExtensionString = (schema, field) => {
383
+ if (!isSchemaObject2(schema))
384
+ return;
385
+ const extension = schema[MJST_EXTENSION_KEY];
386
+ if (typeof extension !== "object" || extension === null)
387
+ return;
388
+ const value = extension[field];
389
+ return typeof value === "string" ? value : undefined;
390
+ };
391
+ var getMjstInstanceOf = (schema) => {
392
+ const instanceOf = readExtensionString(schema, "instanceOf");
393
+ return instanceOf !== undefined && IDENTIFIER.test(instanceOf) ? instanceOf : undefined;
394
+ };
395
+ var getMjstPrimitive = (schema) => {
396
+ const primitive = readExtensionString(schema, "primitive");
397
+ return primitive !== undefined && SUPPORTED_PRIMITIVES.has(primitive) ? primitive : undefined;
398
+ };
399
+ var getMjstBrand = (schema) => {
400
+ const brand = readExtensionString(schema, "brand");
401
+ return brand !== undefined && SAFE_BRAND.test(brand) ? brand : undefined;
402
+ };
378
403
  var toKebabCase4 = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase();
379
404
  var uriRefToFilename4 = (uri) => {
380
405
  const hashIndex = uri.indexOf("#");
@@ -515,13 +540,27 @@ var buildJsDocBlock = (title, description, commentUrl) => {
515
540
  `;
516
541
  return block;
517
542
  };
518
- var getTypeScriptType = (schema) => {
543
+ var getTypeScriptType = (schema, options = {}) => {
544
+ const base = getUnbrandedType(schema, options);
545
+ const brand = getMjstBrand(schema);
546
+ return brand ? `(${base} & { readonly __brand: '${brand}' })` : base;
547
+ };
548
+ var recordType = (keyType, valueType, options) => options.readonly ? `Readonly<Record<${keyType}, ${valueType}>>` : `Record<${keyType}, ${valueType}>`;
549
+ var getUnbrandedType = (schema, options = {}) => {
519
550
  if (typeof schema === "boolean") {
520
551
  return getBooleanSubSchemaType(schema);
521
552
  }
522
553
  if (typeof schema !== "object" || schema === null) {
523
554
  return "unknown";
524
555
  }
556
+ const instanceOf = getMjstInstanceOf(schema);
557
+ if (instanceOf) {
558
+ return instanceOf;
559
+ }
560
+ const primitive = getMjstPrimitive(schema);
561
+ if (primitive) {
562
+ return primitive;
563
+ }
525
564
  if (schema.$ref) {
526
565
  if (!schema.$ref.startsWith("#")) {
527
566
  return "unknown";
@@ -555,29 +594,29 @@ var getTypeScriptType = (schema) => {
555
594
  return multiEnumUnion;
556
595
  }
557
596
  if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
558
- let oneOfUnion = getTypeScriptType(schema.oneOf[0]);
597
+ let oneOfUnion = getTypeScriptType(schema.oneOf[0], options);
559
598
  for (let i = 1;i < schema.oneOf.length; i++) {
560
- oneOfUnion += " | " + getTypeScriptType(schema.oneOf[i]);
599
+ oneOfUnion += " | " + getTypeScriptType(schema.oneOf[i], options);
561
600
  }
562
601
  return oneOfUnion;
563
602
  }
564
603
  if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
565
- let anyOfUnion = getTypeScriptType(schema.anyOf[0]);
604
+ let anyOfUnion = getTypeScriptType(schema.anyOf[0], options);
566
605
  for (let i = 1;i < schema.anyOf.length; i++) {
567
- anyOfUnion += " | " + getTypeScriptType(schema.anyOf[i]);
606
+ anyOfUnion += " | " + getTypeScriptType(schema.anyOf[i], options);
568
607
  }
569
608
  return anyOfUnion;
570
609
  }
571
610
  if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) {
572
- let intersectionTypes = getTypeScriptType(schema.allOf[0]);
611
+ let intersectionTypes = getTypeScriptType(schema.allOf[0], options);
573
612
  for (let i = 1;i < schema.allOf.length; i++) {
574
- intersectionTypes += " & " + getTypeScriptType(schema.allOf[i]);
613
+ intersectionTypes += " & " + getTypeScriptType(schema.allOf[i], options);
575
614
  }
576
615
  return intersectionTypes;
577
616
  }
578
617
  const conditionalResult = getConditionalObjectSchema(schema);
579
618
  if (conditionalResult) {
580
- const baseType = getTypeScriptType(conditionalResult.schema);
619
+ const baseType = getTypeScriptType(conditionalResult.schema, options);
581
620
  if (conditionalResult.thenRef) {
582
621
  return `(${baseType}) & ${refToName2(conditionalResult.thenRef)}`;
583
622
  }
@@ -586,20 +625,20 @@ var getTypeScriptType = (schema) => {
586
625
  if (!schema.type) {
587
626
  if (schema.additionalProperties !== undefined) {
588
627
  if (typeof schema.additionalProperties === "boolean") {
589
- return `Record<string, ${getBooleanSubSchemaType(schema.additionalProperties)}>`;
628
+ return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options);
590
629
  }
591
- return `Record<string, ${getTypeScriptType(schema.additionalProperties)}>`;
630
+ return recordType("string", getTypeScriptType(schema.additionalProperties, options), options);
592
631
  }
593
632
  if (schema.patternProperties && typeof schema.patternProperties === "object") {
594
633
  const firstEntry = Object.entries(schema.patternProperties)[0];
595
634
  if (firstEntry) {
596
635
  const [pattern, value] = firstEntry;
597
636
  if (value !== undefined) {
598
- const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value);
637
+ const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value, options);
599
638
  if (pattern === "^x-") {
600
- return `Record<\`x-\${string}\`, ${valueType}>`;
639
+ return recordType("`x-${string}`", valueType, options);
601
640
  }
602
- return `Record<string, ${valueType}>`;
641
+ return recordType("string", valueType, options);
603
642
  }
604
643
  }
605
644
  }
@@ -652,41 +691,42 @@ var getTypeScriptType = (schema) => {
652
691
  return "boolean";
653
692
  case "array":
654
693
  if (schema.items) {
655
- const itemType = getTypeScriptType(schema.items);
694
+ const itemType = getTypeScriptType(schema.items, options);
656
695
  const wrappedItemType = itemType.includes(" | ") ? `(${itemType})` : itemType;
657
- return `${wrappedItemType}[]`;
696
+ return options.readonly ? `readonly ${wrappedItemType}[]` : `${wrappedItemType}[]`;
658
697
  }
659
- return "unknown[]";
698
+ return options.readonly ? "readonly unknown[]" : "unknown[]";
660
699
  case "object":
661
700
  if (schema.properties) {
701
+ const readonlyPrefix = options.readonly ? "readonly " : "";
662
702
  let properties = "";
663
703
  let first = true;
664
704
  for (const key in schema.properties) {
665
705
  const propSchema = schema.properties[key];
666
706
  const isRequired = schema.required?.includes(key) ?? false;
667
707
  const optional = isRequired ? "" : "?";
668
- const propType = getTypeScriptType(propSchema);
708
+ const propType = getTypeScriptType(propSchema, options);
669
709
  if (!first)
670
710
  properties += "; ";
671
- properties += safeKey(key) + optional + ": " + propType;
711
+ properties += readonlyPrefix + safeKey(key) + optional + ": " + propType;
672
712
  first = false;
673
713
  }
674
714
  return "{ " + properties + " }";
675
715
  }
676
716
  if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
677
- const additionalPropType = getTypeScriptType(schema.additionalProperties);
678
- return `Record<string, ${additionalPropType}>`;
717
+ const additionalPropType = getTypeScriptType(schema.additionalProperties, options);
718
+ return recordType("string", additionalPropType, options);
679
719
  }
680
720
  if (schema.patternProperties && typeof schema.patternProperties === "object") {
681
721
  const firstEntry = Object.entries(schema.patternProperties)[0];
682
722
  if (firstEntry) {
683
723
  const [pattern, patternVal] = firstEntry;
684
724
  if (patternVal) {
685
- const valueType = getTypeScriptType(patternVal);
725
+ const valueType = getTypeScriptType(patternVal, options);
686
726
  if (pattern === "^x-") {
687
- return `Record<\`x-\${string}\`, ${valueType}>`;
727
+ return recordType("`x-${string}`", valueType, options);
688
728
  }
689
- return `Record<string, ${valueType}>`;
729
+ return recordType("string", valueType, options);
690
730
  }
691
731
  }
692
732
  }
@@ -695,9 +735,10 @@ var getTypeScriptType = (schema) => {
695
735
  return "unknown";
696
736
  }
697
737
  };
698
- var generateTypeDefinition = (schema, typeName) => {
738
+ var generateTypeDefinition = (schema, typeName, options = {}) => {
739
+ const readonlyPrefix = options.readonly ? "readonly " : "";
699
740
  if (!isObjectLikeSchema(schema)) {
700
- const tsType = getTypeScriptType(schema);
741
+ const tsType = getTypeScriptType(schema, options);
701
742
  let result = "";
702
743
  if (isSchemaObject2(schema) && schema.$comment && typeof schema.$comment === "string") {
703
744
  result += buildJsDocBlock(typeName, schema.$comment);
@@ -725,23 +766,23 @@ var generateTypeDefinition = (schema, typeName) => {
725
766
  if (firstPatternProperty === undefined) {
726
767
  return `export type ${typeName} = Record<string, unknown>;`;
727
768
  }
728
- const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty);
769
+ const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty, options);
729
770
  const keyType = firstPattern === "^x-" ? "`x-${string}`" : "string";
730
771
  let result2 = "";
731
772
  if (jsDocTitle && jsDocDescription) {
732
773
  result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
733
774
  }
734
- result2 += `export type ${typeName} = Record<${keyType}, ${patternPropType}>;`;
775
+ result2 += `export type ${typeName} = ${recordType(keyType, patternPropType, options)};`;
735
776
  return result2;
736
777
  }
737
778
  if (!hasProperties2 && hasAdditionalProperties2 && normalizedSchema.additionalProperties) {
738
- const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties);
779
+ const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties, options);
739
780
  let result2 = "";
740
781
  if (jsDocTitle && jsDocDescription) {
741
782
  result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
742
783
  }
743
784
  result2 += `export type ${typeName} = {
744
- [key: string]: ${additionalPropType};
785
+ ${readonlyPrefix}[key: string]: ${additionalPropType};
745
786
  };`;
746
787
  return result2;
747
788
  }
@@ -752,8 +793,8 @@ var generateTypeDefinition = (schema, typeName) => {
752
793
  const propSchema = schemaProps[key];
753
794
  const isRequired = normalizedSchema.required?.includes(key) ?? false;
754
795
  const optional = isRequired ? "" : "?";
755
- const propType = getTypeScriptType(propSchema);
756
- const quotedKey = safeKey(key);
796
+ const propType = getTypeScriptType(propSchema, options);
797
+ const quotedKey = readonlyPrefix + safeKey(key);
757
798
  if (!isFirstProp)
758
799
  properties += `
759
800
  `;
@@ -803,31 +844,32 @@ var PACKAGE_IMPORTS = {
803
844
  "validate-record": "import { validateRecord } from '@amritk/helpers/validate-record';",
804
845
  "has-ref": "import { hasRef } from '@amritk/helpers/schema-guards';"
805
846
  };
806
- var EMBEDDED_IMPORTS = {
807
- "is-object": "import { isObject } from './_helpers/is-object';",
808
- "validate-array": "import { validateArray } from './_helpers/validate-array';",
809
- "validate-record": "import { validateRecord } from './_helpers/validate-record';",
810
- "has-ref": "import { hasRef } from './_helpers/has-ref';"
847
+ var EMBEDDED_NAMED_EXPORTS = {
848
+ "is-object": "isObject",
849
+ "validate-array": "validateArray",
850
+ "validate-record": "validateRecord",
851
+ "has-ref": "hasRef"
811
852
  };
812
- var collectHelpers = (parserFunction, mode) => {
853
+ var embeddedImport = (helper, prefix) => `import { ${EMBEDDED_NAMED_EXPORTS[helper]} } from '${prefix}_helpers/${helper}';`;
854
+ var collectHelpers = (parserFunction, mode, helpersImportPrefix = "./") => {
813
855
  const imports = [];
814
856
  const used = new Set;
815
- const table = mode === "embedded" ? EMBEDDED_IMPORTS : PACKAGE_IMPORTS;
857
+ const importFor = (helper) => mode === "embedded" ? embeddedImport(helper, helpersImportPrefix) : PACKAGE_IMPORTS[helper];
816
858
  if (parserFunction.includes("validateArray")) {
817
- imports.push(table["validate-array"]);
859
+ imports.push(importFor("validate-array"));
818
860
  used.add("validate-array");
819
861
  }
820
862
  if (parserFunction.includes("validateRecord")) {
821
- imports.push(table["validate-record"]);
863
+ imports.push(importFor("validate-record"));
822
864
  used.add("validate-record");
823
865
  used.add("is-object");
824
866
  }
825
867
  if (parserFunction.includes("isObject")) {
826
- imports.push(table["is-object"]);
868
+ imports.push(importFor("is-object"));
827
869
  used.add("is-object");
828
870
  }
829
871
  if (parserFunction.includes("hasRef(")) {
830
- imports.push(table["has-ref"]);
872
+ imports.push(importFor("has-ref"));
831
873
  used.add("has-ref");
832
874
  }
833
875
  return { imports, used };
@@ -1082,6 +1124,31 @@ var safeKey2 = (key) => {
1082
1124
  return `'${key}'`;
1083
1125
  };
1084
1126
 
1127
+ // ../helpers/dist/mjst-extension.js
1128
+ var isSchemaObject4 = (schema) => {
1129
+ return typeof schema === "object" && schema !== null && typeof schema !== "boolean";
1130
+ };
1131
+ var MJST_EXTENSION_KEY2 = "x-mjst";
1132
+ var IDENTIFIER2 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1133
+ var SUPPORTED_PRIMITIVES2 = new Set(["bigint"]);
1134
+ var readExtensionString2 = (schema, field) => {
1135
+ if (!isSchemaObject4(schema))
1136
+ return;
1137
+ const extension = schema[MJST_EXTENSION_KEY2];
1138
+ if (typeof extension !== "object" || extension === null)
1139
+ return;
1140
+ const value = extension[field];
1141
+ return typeof value === "string" ? value : undefined;
1142
+ };
1143
+ var getMjstInstanceOf2 = (schema) => {
1144
+ const instanceOf = readExtensionString2(schema, "instanceOf");
1145
+ return instanceOf !== undefined && IDENTIFIER2.test(instanceOf) ? instanceOf : undefined;
1146
+ };
1147
+ var getMjstPrimitive2 = (schema) => {
1148
+ const primitive = readExtensionString2(schema, "primitive");
1149
+ return primitive !== undefined && SUPPORTED_PRIMITIVES2.has(primitive) ? primitive : undefined;
1150
+ };
1151
+
1085
1152
  // src/generators/generate-default-from-pattern.ts
1086
1153
  var generateDefaultFromPattern = (pattern) => {
1087
1154
  if (pattern.includes("@") || pattern.includes("[a-zA-Z0-9]") && pattern.includes("\\.")) {
@@ -1139,6 +1206,9 @@ var getDefaultValue = (schema) => {
1139
1206
  return getDefaultValue(firstSchema);
1140
1207
  }
1141
1208
  }
1209
+ if (getMjstPrimitive2(schema) === "bigint") {
1210
+ return "0n";
1211
+ }
1142
1212
  if (!hasType(schema)) {
1143
1213
  return "undefined";
1144
1214
  }
@@ -1234,6 +1304,24 @@ var generatePropertyAssertion = (key, propSchema, isRequired, typeName) => {
1234
1304
  return lines;
1235
1305
  if (hasRef(propSchema))
1236
1306
  return lines;
1307
+ const instanceOf = getMjstInstanceOf2(propSchema);
1308
+ if (instanceOf) {
1309
+ if (isRequired) {
1310
+ lines.push(` if (!(${acc} instanceof ${instanceOf})) throw new Error('[${typeName}] field "${key}" must be ${instanceOf}');`);
1311
+ } else {
1312
+ lines.push(` if (${acc} !== undefined && !(${acc} instanceof ${instanceOf})) throw new Error('[${typeName}] field "${key}" must be ${instanceOf}');`);
1313
+ }
1314
+ return lines;
1315
+ }
1316
+ const primitive = getMjstPrimitive2(propSchema);
1317
+ if (primitive) {
1318
+ if (isRequired) {
1319
+ lines.push(` if (typeof ${acc} !== "${primitive}") throw new Error('[${typeName}] field "${key}" must be ${primitive}');`);
1320
+ } else {
1321
+ lines.push(` if (${acc} !== undefined && typeof ${acc} !== "${primitive}") throw new Error('[${typeName}] field "${key}" must be ${primitive}');`);
1322
+ }
1323
+ return lines;
1324
+ }
1237
1325
  if (hasEnum(propSchema)) {
1238
1326
  const allowed = JSON.stringify(propSchema.enum);
1239
1327
  const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
@@ -1270,6 +1358,14 @@ var generateObjectStrictAssertion = (schema, typeName) => {
1270
1358
  return lines;
1271
1359
  };
1272
1360
  var generateScalarStrictAssertion = (schema, typeName) => {
1361
+ const instanceOf = getMjstInstanceOf2(schema);
1362
+ if (instanceOf) {
1363
+ return ` if (!(input instanceof ${instanceOf})) throw new Error(\`[${typeName}] expected ${instanceOf}, got \${input === null ? "null" : typeof input}\`);`;
1364
+ }
1365
+ const primitive = getMjstPrimitive2(schema);
1366
+ if (primitive) {
1367
+ return ` if (typeof input !== "${primitive}") throw new Error(\`[${typeName}] expected ${primitive}, got \${input === null ? "null" : typeof input}\`);`;
1368
+ }
1273
1369
  if (!isSchemaObject3(schema) || !hasType(schema))
1274
1370
  return null;
1275
1371
  const t = schema.type;
@@ -1329,6 +1425,14 @@ var generateEnumCheck = (accessor, enumValues) => {
1329
1425
  // src/generators/generate-schema-checks.ts
1330
1426
  var generateSchemaChecks = (accessor, schema) => {
1331
1427
  const checks = [];
1428
+ const instanceOf = getMjstInstanceOf2(schema);
1429
+ if (instanceOf) {
1430
+ return [`${accessor} instanceof ${instanceOf}`];
1431
+ }
1432
+ const primitive = getMjstPrimitive2(schema);
1433
+ if (primitive) {
1434
+ return [`typeof ${accessor} === "${primitive}"`];
1435
+ }
1332
1436
  if (!hasType(schema)) {
1333
1437
  return checks;
1334
1438
  }
@@ -1461,6 +1565,11 @@ var generateNonDiscriminatedUnionValidation = (accessor, schemas, defaultValue,
1461
1565
  };
1462
1566
 
1463
1567
  // src/generators/generate-validation-expression.ts
1568
+ var getInstanceCoercion = (accessor, instanceOf) => {
1569
+ if (instanceOf === "Date")
1570
+ return `new Date(${accessor} as string | number | Date)`;
1571
+ return null;
1572
+ };
1464
1573
  var getTypeCoercion = (accessor, schema, defaultValue) => {
1465
1574
  if (!hasType(schema)) {
1466
1575
  return null;
@@ -1489,6 +1598,20 @@ var generateValidationExpression = (key, schema, defaultValue, isRequired, rootS
1489
1598
  if (!isSchemaObject3(schema)) {
1490
1599
  return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
1491
1600
  }
1601
+ const instanceOf = getMjstInstanceOf2(schema);
1602
+ if (instanceOf) {
1603
+ const valid = `${accessor} instanceof ${instanceOf}`;
1604
+ const coercion = getInstanceCoercion(accessor, instanceOf);
1605
+ if (isRequired) {
1606
+ return coercion ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : ${defaultValue})` : `${valid} ? ${accessor} : ${defaultValue}`;
1607
+ }
1608
+ return coercion ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : undefined)` : `${valid} ? ${accessor} : undefined`;
1609
+ }
1610
+ const primitive = getMjstPrimitive2(schema);
1611
+ if (primitive) {
1612
+ const valid = `typeof ${accessor} === "${primitive}"`;
1613
+ return isRequired ? `${valid} ? ${accessor} : ${defaultValue}` : `${valid} ? ${accessor} : undefined`;
1614
+ }
1492
1615
  if (hasRef(schema) && rootSchema) {
1493
1616
  const ref = schema.$ref;
1494
1617
  if (visitedRefs?.has(ref)) {
@@ -2443,7 +2566,7 @@ var generateFile = (schema, typeName, options) => {
2443
2566
  const selfRef = options?.selfRef;
2444
2567
  const rootSchema = options?.rootSchema;
2445
2568
  const helpersMode = options?.helpersMode ?? "package";
2446
- const typeDefinition = generateTypeDefinition(schema, typeName);
2569
+ const typeDefinition = generateTypeDefinition(schema, typeName, { readonly: options?.readonly ?? false });
2447
2570
  if (typesOnly) {
2448
2571
  const imports2 = collectImports(schema, { typesOnly: true, selfRef, rootSchema });
2449
2572
  let result2 = "";
@@ -2468,7 +2591,7 @@ var generateFile = (schema, typeName, options) => {
2468
2591
  const combinedFunctions = `${shapeValidator}
2469
2592
 
2470
2593
  ${parserFunction}`;
2471
- const helpers = collectHelpers(combinedFunctions, helpersMode);
2594
+ const helpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix);
2472
2595
  const imports = [...collectImports(schema, { selfRef, rootSchema }), ...helpers.imports];
2473
2596
  let result = "";
2474
2597
  if (imports.length > 0) {
@@ -2512,7 +2635,7 @@ var extractDynamicAnchorDefs = (schema) => {
2512
2635
  }
2513
2636
  return refs;
2514
2637
  };
2515
- var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package") => {
2638
+ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWarnings, strict, helpersMode = "package", helpersImportPrefix = "./", readonly = false) => {
2516
2639
  rootSchema = upgradeDraft07Schema(rootSchema);
2517
2640
  const files = [];
2518
2641
  const processedRefs = new Set;
@@ -2526,6 +2649,8 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
2526
2649
  typesOnly: typesOnly ?? false,
2527
2650
  rootSchema,
2528
2651
  helpersMode,
2652
+ helpersImportPrefix,
2653
+ readonly,
2529
2654
  ...logWarnings !== undefined ? { logWarnings } : {},
2530
2655
  ...strict !== undefined ? { strict } : {}
2531
2656
  });
@@ -2561,6 +2686,8 @@ var buildSchema = async (rootSchema, rootTypeName, extensions, typesOnly, logWar
2561
2686
  selfRef: ref,
2562
2687
  rootSchema,
2563
2688
  helpersMode,
2689
+ helpersImportPrefix,
2690
+ readonly,
2564
2691
  ...logWarnings !== undefined ? { logWarnings } : {},
2565
2692
  ...strict !== undefined ? { strict } : {}
2566
2693
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -48,8 +48,8 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "json-schema-typed": "catalog:",
52
- "@amritk/helpers": "workspace:*",
53
- "@amritk/generate-markdown": "workspace:*"
51
+ "json-schema-typed": "^8.0.1",
52
+ "@amritk/helpers": "0.5.0",
53
+ "@amritk/generate-markdown": "0.2.1"
54
54
  }
55
55
  }
@@ -681,6 +681,30 @@ describe('build-schema', () => {
681
681
  expect(filenames).toContain('_helpers/is-object.ts')
682
682
  })
683
683
 
684
+ it('honours a custom helpersImportPrefix so nested parsers import from a shared root _helpers/', async () => {
685
+ const schema: JSONSchema = {
686
+ type: 'object',
687
+ properties: { name: { type: 'string' } },
688
+ }
689
+
690
+ const result = await buildSchema(
691
+ schema,
692
+ 'Document',
693
+ undefined,
694
+ undefined,
695
+ undefined,
696
+ undefined,
697
+ 'embedded',
698
+ '../../',
699
+ )
700
+ const root = result.find((f) => f.filename === 'document.ts')
701
+ const filenames = result.map((f) => f.filename)
702
+
703
+ expect(root?.content).toContain("import { isObject } from '../../_helpers/is-object';")
704
+ // Helper sources keep their canonical filenames; the orchestrator relocates them to the root.
705
+ expect(filenames).toContain('_helpers/is-object.ts')
706
+ })
707
+
684
708
  it('ships validate-array.ts (plus its transitive is-object.ts) for array schemas', async () => {
685
709
  const schema: JSONSchema = {
686
710
  type: 'object',
@@ -90,6 +90,12 @@ const extractDynamicAnchorDefs = (schema: JSONSchema): string[] => {
90
90
  * @param helpersMode - `'package'` (default) emits `import ... from '@amritk/helpers/...'`.
91
91
  * `'embedded'` emits `import ... from './_helpers/...'` and appends the helper sources
92
92
  * as additional `GeneratedFile` entries so the output directory is self-contained.
93
+ * @param helpersImportPrefix - Relative path prefix to the `_helpers/` directory in
94
+ * embedded mode. Defaults to `'./'`. The recursive multi-schema build passes `'../'`,
95
+ * `'../../'`, etc. so nested parsers can import from a single shared `_helpers/`
96
+ * directory while the helper sources are emitted once at the output root.
97
+ * @param readonly - When true, every property, array, and record in the generated type
98
+ * definitions is emitted as `readonly`.
93
99
  * @returns An array of generated TypeScript files
94
100
  *
95
101
  * @example
@@ -130,6 +136,8 @@ export const buildSchema = async (
130
136
  logWarnings?: boolean,
131
137
  strict?: boolean,
132
138
  helpersMode: HelpersMode = 'package',
139
+ helpersImportPrefix = './',
140
+ readonly = false,
133
141
  ): Promise<GeneratedFile[]> => {
134
142
  // Upgrade draft-07 schemas to 2020-12 conventions before processing.
135
143
  // This renames `definitions` → `$defs` recursively so the rest of the
@@ -155,6 +163,8 @@ export const buildSchema = async (
155
163
  typesOnly: typesOnly ?? false,
156
164
  rootSchema: rootSchema as Record<string, unknown>,
157
165
  helpersMode,
166
+ helpersImportPrefix,
167
+ readonly,
158
168
  ...(logWarnings !== undefined ? { logWarnings } : {}),
159
169
  ...(strict !== undefined ? { strict } : {}),
160
170
  })
@@ -205,6 +215,8 @@ export const buildSchema = async (
205
215
  selfRef: ref,
206
216
  rootSchema: rootSchema as Record<string, unknown>,
207
217
  helpersMode,
218
+ helpersImportPrefix,
219
+ readonly,
208
220
  ...(logWarnings !== undefined ? { logWarnings } : {}),
209
221
  ...(strict !== undefined ? { strict } : {}),
210
222
  })
@@ -47,6 +47,18 @@ type GenerateFileOptions = {
47
47
  * be shipped alongside the generated files.
48
48
  */
49
49
  readonly helpersMode?: HelpersMode
50
+ /**
51
+ * Relative path prefix to the shared `_helpers/` directory in embedded mode.
52
+ * Defaults to `'./'`. The recursive multi-schema build passes `'../'`,
53
+ * `'../../'`, etc. so a nested parser can reach a single `_helpers/` directory
54
+ * at the output root.
55
+ */
56
+ readonly helpersImportPrefix?: string
57
+ /**
58
+ * When true, every property, array, and record in the generated type
59
+ * definitions is emitted as `readonly`.
60
+ */
61
+ readonly readonly?: boolean
50
62
  }
51
63
 
52
64
  /** Result of generating a single parser file. */
@@ -93,7 +105,7 @@ export const generateFile = (
93
105
  const selfRef = options?.selfRef
94
106
  const rootSchema = options?.rootSchema
95
107
  const helpersMode: HelpersMode = options?.helpersMode ?? 'package'
96
- const typeDefinition = generateTypeDefinition(schema, typeName)
108
+ const typeDefinition = generateTypeDefinition(schema, typeName, { readonly: options?.readonly ?? false })
97
109
 
98
110
  if (typesOnly) {
99
111
  // In types-only mode, skip the parser function and use type-only imports
@@ -118,7 +130,7 @@ export const generateFile = (
118
130
  })
119
131
  const shapeValidator = generateShapeValidator(schema, typeName, true)
120
132
  const combinedFunctions = `${shapeValidator}\n\n${parserFunction}`
121
- const helpers: CollectedHelpers = collectHelpers(combinedFunctions, helpersMode)
133
+ const helpers: CollectedHelpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix)
122
134
  const imports = [...collectImports(schema, { selfRef, rootSchema }), ...helpers.imports]
123
135
 
124
136
  // Build file output using string concatenation instead of array join for performance
@@ -319,4 +319,12 @@ describe('generate-schema-checks', () => {
319
319
  const result = generateSchemaChecks('input?.name', { type: 'string', minLength: 1 })
320
320
  expect(result).toEqual(['typeof input?.name === "string"', 'input?.name.length >= 1'])
321
321
  })
322
+
323
+ it('returns an instanceof check for an x-mjst instanceOf node', () => {
324
+ expect(generateSchemaChecks('value', { 'x-mjst': { instanceOf: 'Date' } })).toEqual(['value instanceof Date'])
325
+ })
326
+
327
+ it('returns a typeof check for an x-mjst bigint node', () => {
328
+ expect(generateSchemaChecks('value', { 'x-mjst': { primitive: 'bigint' } })).toEqual(['typeof value === "bigint"'])
329
+ })
322
330
  })
@@ -1,3 +1,4 @@
1
+ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
1
2
  import {
2
3
  hasAdditionalProperties,
3
4
  hasEnum,
@@ -28,6 +29,18 @@ import { generateEnumCheck } from './generate-enum-check'
28
29
  export const generateSchemaChecks = (accessor: string, schema: JSONSchema): string[] => {
29
30
  const checks: string[] = []
30
31
 
32
+ // x-mjst instanceOf nodes validate purely by instance check (no native type).
33
+ const instanceOf = getMjstInstanceOf(schema)
34
+ if (instanceOf) {
35
+ return [`${accessor} instanceof ${instanceOf}`]
36
+ }
37
+
38
+ // x-mjst primitive nodes (e.g. bigint) validate purely by typeof check.
39
+ const primitive = getMjstPrimitive(schema)
40
+ if (primitive) {
41
+ return [`typeof ${accessor} === "${primitive}"`]
42
+ }
43
+
31
44
  if (!hasType(schema)) {
32
45
  return checks
33
46
  }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion'
4
+
5
+ describe('generate-strict-assertion x-mjst instanceOf', () => {
6
+ it('asserts instanceof for a required Date property', () => {
7
+ const schema = {
8
+ type: 'object' as const,
9
+ properties: { createdAt: { 'x-mjst': { instanceOf: 'Date' } } },
10
+ required: ['createdAt'],
11
+ }
12
+ const lines = generateObjectStrictAssertion(schema, 'Event').join('\n')
13
+
14
+ expect(lines).toContain('missing required property "createdAt"')
15
+ expect(lines).toContain('!(input.createdAt instanceof Date)')
16
+ expect(lines).toContain('field "createdAt" must be Date')
17
+ })
18
+
19
+ it('guards undefined before asserting instanceof for an optional Date property', () => {
20
+ const schema = {
21
+ type: 'object' as const,
22
+ properties: { createdAt: { 'x-mjst': { instanceOf: 'Date' } } },
23
+ }
24
+ const lines = generateObjectStrictAssertion(schema, 'Event').join('\n')
25
+
26
+ expect(lines).toContain('input.createdAt !== undefined && !(input.createdAt instanceof Date)')
27
+ })
28
+
29
+ it('asserts instanceof for a top-level Date schema', () => {
30
+ const line = generateScalarStrictAssertion({ 'x-mjst': { instanceOf: 'Date' } }, 'When')
31
+
32
+ expect(line).toContain('!(input instanceof Date)')
33
+ expect(line).toContain('expected Date')
34
+ })
35
+ })
36
+
37
+ describe('generate-strict-assertion x-mjst primitive', () => {
38
+ it('asserts typeof bigint for a required property', () => {
39
+ const schema = {
40
+ type: 'object' as const,
41
+ properties: { balance: { 'x-mjst': { primitive: 'bigint' } } },
42
+ required: ['balance'],
43
+ }
44
+ const lines = generateObjectStrictAssertion(schema, 'Account').join('\n')
45
+
46
+ expect(lines).toContain('missing required property "balance"')
47
+ expect(lines).toContain('typeof input.balance !== "bigint"')
48
+ expect(lines).toContain('field "balance" must be bigint')
49
+ })
50
+
51
+ it('guards undefined before asserting typeof for an optional bigint property', () => {
52
+ const schema = {
53
+ type: 'object' as const,
54
+ properties: { balance: { 'x-mjst': { primitive: 'bigint' } } },
55
+ }
56
+ const lines = generateObjectStrictAssertion(schema, 'Account').join('\n')
57
+
58
+ expect(lines).toContain('input.balance !== undefined && typeof input.balance !== "bigint"')
59
+ })
60
+
61
+ it('asserts typeof for a top-level bigint schema', () => {
62
+ const line = generateScalarStrictAssertion({ 'x-mjst': { primitive: 'bigint' } }, 'Big')
63
+
64
+ expect(line).toContain('typeof input !== "bigint"')
65
+ expect(line).toContain('expected bigint')
66
+ })
67
+ })
@@ -1,3 +1,4 @@
1
+ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
1
2
  import { safeAccessor } from '@amritk/helpers/safe-accessor'
2
3
  import {
3
4
  hasEnum,
@@ -134,6 +135,34 @@ const generatePropertyAssertion = (
134
135
  if (!isSchemaObject(propSchema)) return lines
135
136
  if (hasRef(propSchema)) return lines
136
137
 
138
+ const instanceOf = getMjstInstanceOf(propSchema)
139
+ if (instanceOf) {
140
+ if (isRequired) {
141
+ lines.push(
142
+ ` if (!(${acc} instanceof ${instanceOf})) throw new Error('[${typeName}] field "${key}" must be ${instanceOf}');`,
143
+ )
144
+ } else {
145
+ lines.push(
146
+ ` if (${acc} !== undefined && !(${acc} instanceof ${instanceOf})) throw new Error('[${typeName}] field "${key}" must be ${instanceOf}');`,
147
+ )
148
+ }
149
+ return lines
150
+ }
151
+
152
+ const primitive = getMjstPrimitive(propSchema)
153
+ if (primitive) {
154
+ if (isRequired) {
155
+ lines.push(
156
+ ` if (typeof ${acc} !== "${primitive}") throw new Error('[${typeName}] field "${key}" must be ${primitive}');`,
157
+ )
158
+ } else {
159
+ lines.push(
160
+ ` if (${acc} !== undefined && typeof ${acc} !== "${primitive}") throw new Error('[${typeName}] field "${key}" must be ${primitive}');`,
161
+ )
162
+ }
163
+ return lines
164
+ }
165
+
137
166
  if (hasEnum(propSchema)) {
138
167
  const allowed = JSON.stringify(propSchema.enum)
139
168
  const label = (propSchema.enum as unknown[]).map((v) => JSON.stringify(v)).join(', ')
@@ -202,6 +231,16 @@ export const generateObjectStrictAssertion = (schema: JSONSchema, typeName: stri
202
231
  * Returns null when the schema has no type information to assert on.
203
232
  */
204
233
  export const generateScalarStrictAssertion = (schema: JSONSchema, typeName: string): string | null => {
234
+ const instanceOf = getMjstInstanceOf(schema)
235
+ if (instanceOf) {
236
+ return ` if (!(input instanceof ${instanceOf})) throw new Error(\`[${typeName}] expected ${instanceOf}, got \${input === null ? "null" : typeof input}\`);`
237
+ }
238
+
239
+ const primitive = getMjstPrimitive(schema)
240
+ if (primitive) {
241
+ return ` if (typeof input !== "${primitive}") throw new Error(\`[${typeName}] expected ${primitive}, got \${input === null ? "null" : typeof input}\`);`
242
+ }
243
+
205
244
  if (!isSchemaObject(schema) || !hasType(schema)) return null
206
245
  const t = schema.type as string
207
246
  const wrongType = wrongTypeCondition('input', t)
@@ -791,4 +791,41 @@ describe('generate-validation-expression', () => {
791
791
 
792
792
  expect(result).toContain('? input?.status : undefined')
793
793
  })
794
+
795
+ it('checks instanceof and coerces a required x-mjst Date field', () => {
796
+ const schema = { 'x-mjst': { instanceOf: 'Date' } }
797
+ const result = generateValidationExpression('createdAt', schema, '{}', true)
798
+
799
+ expect(result).toContain('input?.createdAt instanceof Date')
800
+ expect(result).toContain('new Date(input?.createdAt as string | number | Date)')
801
+ })
802
+
803
+ it('checks instanceof and coerces an optional x-mjst Date field', () => {
804
+ const schema = { 'x-mjst': { instanceOf: 'Date' } }
805
+ const result = generateValidationExpression('createdAt', schema, 'undefined', false)
806
+
807
+ expect(result).toContain('input?.createdAt instanceof Date')
808
+ expect(result).toContain('? input?.createdAt : (input?.createdAt !== undefined ? new Date')
809
+ })
810
+
811
+ it('falls back to the default for an unknown instanceOf with no coercion', () => {
812
+ const schema = { 'x-mjst': { instanceOf: 'CustomThing' } }
813
+ const result = generateValidationExpression('value', schema, '{}', true)
814
+
815
+ expect(result).toBe('input?.value instanceof CustomThing ? input?.value : {}')
816
+ })
817
+
818
+ it('checks typeof bigint for a required x-mjst primitive field', () => {
819
+ const schema = { 'x-mjst': { primitive: 'bigint' } }
820
+ const result = generateValidationExpression('balance', schema, '0n', true)
821
+
822
+ expect(result).toBe('typeof input?.balance === "bigint" ? input?.balance : 0n')
823
+ })
824
+
825
+ it('checks typeof bigint for an optional x-mjst primitive field', () => {
826
+ const schema = { 'x-mjst': { primitive: 'bigint' } }
827
+ const result = generateValidationExpression('balance', schema, '0n', false)
828
+
829
+ expect(result).toBe('typeof input?.balance === "bigint" ? input?.balance : undefined')
830
+ })
794
831
  })
@@ -1,3 +1,4 @@
1
+ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
1
2
  import { resolveRef } from '@amritk/helpers/resolve-ref'
2
3
  import { safeAccessor } from '@amritk/helpers/safe-accessor'
3
4
  import {
@@ -34,6 +35,17 @@ import { generateEnumCheck } from './generate-enum-check'
34
35
  import { generateNonDiscriminatedUnionValidation } from './generate-non-discriminated-union-validation'
35
36
  import { generateSchemaChecks } from './generate-schema-checks'
36
37
 
38
+ /**
39
+ * Coercion expression for an `x-mjst` instanceOf value, when one is known.
40
+ * `Date` can be reconstructed from the common JSON encodings (ISO string or
41
+ * epoch number); other classes have no safe generic coercion, so we return null
42
+ * and the caller falls back to the default.
43
+ */
44
+ const getInstanceCoercion = (accessor: string, instanceOf: string): string | null => {
45
+ if (instanceOf === 'Date') return `new Date(${accessor} as string | number | Date)`
46
+ return null
47
+ }
48
+
37
49
  /**
38
50
  * Generates a type coercion expression for converting a value to the expected type.
39
51
  */
@@ -85,6 +97,34 @@ export const generateValidationExpression = (
85
97
  return isRequired ? `${accessor} ?? ${defaultValue}` : accessor
86
98
  }
87
99
 
100
+ // Handle x-mjst instanceOf (e.g. Date): the value must be an instance of the
101
+ // named class. Invalid values are coerced when a coercer is known, otherwise
102
+ // they fall back to the default (required) or undefined (optional).
103
+ const instanceOf = getMjstInstanceOf(schema)
104
+ if (instanceOf) {
105
+ const valid = `${accessor} instanceof ${instanceOf}`
106
+ const coercion = getInstanceCoercion(accessor, instanceOf)
107
+
108
+ if (isRequired) {
109
+ return coercion
110
+ ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : ${defaultValue})`
111
+ : `${valid} ? ${accessor} : ${defaultValue}`
112
+ }
113
+ return coercion
114
+ ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : undefined)`
115
+ : `${valid} ? ${accessor} : undefined`
116
+ }
117
+
118
+ // Handle x-mjst primitive (e.g. bigint): validate with typeof. We do not
119
+ // coerce — BigInt() throws on invalid input, which a pure coercion expression
120
+ // cannot guard — so invalid values fall back to the default (required) or
121
+ // undefined (optional).
122
+ const primitive = getMjstPrimitive(schema)
123
+ if (primitive) {
124
+ const valid = `typeof ${accessor} === "${primitive}"`
125
+ return isRequired ? `${valid} ? ${accessor} : ${defaultValue}` : `${valid} ? ${accessor} : undefined`
126
+ }
127
+
88
128
  // Handle $ref - resolve and validate against the referenced schema
89
129
  if (hasRef(schema) && rootSchema) {
90
130
  const ref = schema.$ref
@@ -99,5 +99,17 @@ describe('collect-helpers', () => {
99
99
  expect(result.imports).toContain("import { validateRecord } from './_helpers/validate-record';")
100
100
  expect(result.imports).toContain("import { isObject } from './_helpers/is-object';")
101
101
  })
102
+
103
+ it('applies a custom import prefix so nested parsers can reach a shared _helpers/ at the root', () => {
104
+ const parser = 'if (isObject(input)) { const arr = validateArray(input.items, parseItem) }'
105
+ const result = collectHelpers(parser, 'embedded', '../../')
106
+ expect(result.imports).toContain("import { isObject } from '../../_helpers/is-object';")
107
+ expect(result.imports).toContain("import { validateArray } from '../../_helpers/validate-array';")
108
+ })
109
+
110
+ it('ignores the import prefix in package mode', () => {
111
+ const result = collectHelpers('if (isObject(input)) {', 'package', '../')
112
+ expect(result.imports).toEqual(["import { isObject } from '@amritk/helpers/is-object';"])
113
+ })
102
114
  })
103
115
  })
@@ -23,26 +23,41 @@ const PACKAGE_IMPORTS: Record<RuntimeHelperName, string> = {
23
23
  'has-ref': "import { hasRef } from '@amritk/helpers/schema-guards';",
24
24
  }
25
25
 
26
- const EMBEDDED_IMPORTS: Record<RuntimeHelperName, string> = {
27
- 'is-object': "import { isObject } from './_helpers/is-object';",
28
- 'validate-array': "import { validateArray } from './_helpers/validate-array';",
29
- 'validate-record': "import { validateRecord } from './_helpers/validate-record';",
30
- 'has-ref': "import { hasRef } from './_helpers/has-ref';",
26
+ const EMBEDDED_NAMED_EXPORTS: Record<RuntimeHelperName, string> = {
27
+ 'is-object': 'isObject',
28
+ 'validate-array': 'validateArray',
29
+ 'validate-record': 'validateRecord',
30
+ 'has-ref': 'hasRef',
31
31
  }
32
32
 
33
- /** Detects which runtime helpers a generated parser body references. */
34
- export const collectHelpers = (parserFunction: string, mode: HelpersMode): CollectedHelpers => {
33
+ const embeddedImport = (helper: RuntimeHelperName, prefix: string): string =>
34
+ `import { ${EMBEDDED_NAMED_EXPORTS[helper]} } from '${prefix}_helpers/${helper}';`
35
+
36
+ /**
37
+ * Detects which runtime helpers a generated parser body references.
38
+ *
39
+ * @param helpersImportPrefix - Relative path prefix to the shared `_helpers/`
40
+ * directory in embedded mode. Defaults to `'./'`. The recursive multi-schema
41
+ * build passes `'../'`, `'../../'`, etc. so nested parsers can reach a single
42
+ * `_helpers/` directory at the output root.
43
+ */
44
+ export const collectHelpers = (
45
+ parserFunction: string,
46
+ mode: HelpersMode,
47
+ helpersImportPrefix = './',
48
+ ): CollectedHelpers => {
35
49
  const imports: string[] = []
36
50
  const used = new Set<RuntimeHelperName>()
37
- const table = mode === 'embedded' ? EMBEDDED_IMPORTS : PACKAGE_IMPORTS
51
+ const importFor = (helper: RuntimeHelperName): string =>
52
+ mode === 'embedded' ? embeddedImport(helper, helpersImportPrefix) : PACKAGE_IMPORTS[helper]
38
53
 
39
54
  if (parserFunction.includes('validateArray')) {
40
- imports.push(table['validate-array'])
55
+ imports.push(importFor('validate-array'))
41
56
  used.add('validate-array')
42
57
  }
43
58
 
44
59
  if (parserFunction.includes('validateRecord')) {
45
- imports.push(table['validate-record'])
60
+ imports.push(importFor('validate-record'))
46
61
  used.add('validate-record')
47
62
  // The embedded validate-record.ts imports is-object, so it must be shipped too —
48
63
  // but only the `validate-record` line is needed in the parser file unless the
@@ -51,12 +66,12 @@ export const collectHelpers = (parserFunction: string, mode: HelpersMode): Colle
51
66
  }
52
67
 
53
68
  if (parserFunction.includes('isObject')) {
54
- imports.push(table['is-object'])
69
+ imports.push(importFor('is-object'))
55
70
  used.add('is-object')
56
71
  }
57
72
 
58
73
  if (parserFunction.includes('hasRef(')) {
59
- imports.push(table['has-ref'])
74
+ imports.push(importFor('has-ref'))
60
75
  used.add('has-ref')
61
76
  }
62
77
 
@@ -132,4 +132,8 @@ describe('get-default-value', () => {
132
132
  it('handles empty oneOf array and falls back', () => {
133
133
  expect(getDefaultValue({ oneOf: [], type: 'boolean' })).toBe('false')
134
134
  })
135
+
136
+ it('returns 0n for an x-mjst bigint schema', () => {
137
+ expect(getDefaultValue({ 'x-mjst': { primitive: 'bigint' } })).toBe('0n')
138
+ })
135
139
  })
@@ -1,3 +1,4 @@
1
+ import { getMjstPrimitive } from '@amritk/helpers/mjst-extension'
1
2
  import {
2
3
  hasAllOf,
3
4
  hasAnyOf,
@@ -60,6 +61,11 @@ export const getDefaultValue = (schema: JSONSchema): string => {
60
61
  }
61
62
  }
62
63
 
64
+ // x-mjst bigint has no native JSON type but a sensible zero default.
65
+ if (getMjstPrimitive(schema) === 'bigint') {
66
+ return '0n'
67
+ }
68
+
63
69
  if (!hasType(schema)) {
64
70
  return 'undefined'
65
71
  }
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export { buildSchema } from './generators/build-schema'
1
+ export { buildSchema, type GeneratedFile } from './generators/build-schema'