@amritk/generate-parsers 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,17 @@ const readHelperSource = async (helper) => {
21
21
  const helpersRoot = dirname(require.resolve('@amritk/helpers/package.json'));
22
22
  try {
23
23
  const source = await readFile(resolvePath(helpersRoot, 'src', `${helper}.ts`), 'utf-8');
24
- return { ext: 'ts', content: source.replace(/from '(/./[^'".]+)'/g, "from '$1.js'") };
24
+ // The leading `\b` in this pattern is load-bearing beyond its regex meaning.
25
+ // Our build runs tsc-alias --resolveFullPaths over the compiled output, and its
26
+ // scanner rewrites anything that looks like `from '<path>'` — even inside a
27
+ // regex literal. Without the `\b`, normalize-path turned this pattern's
28
+ // backslashes into slashes and v0.12.3 shipped with an unparseable regex that
29
+ // crashed the CLI on load. The `\b` blocks that scan (in the emitted text,
30
+ // `from` is preceded by the word character `b`, so tsc-alias's own `\bfrom`
31
+ // never matches) and is also semantically correct: an import keyword always
32
+ // sits at a word boundary. The dist smoke test (scripts/dist-smoke.test.ts)
33
+ // fails the build if this ever regresses.
34
+ return { ext: 'ts', content: source.replace(/\bfrom '(\.\/[^'".]+)'/g, "from '$1.js'") };
25
35
  }
26
36
  catch {
27
37
  const compiled = await readFile(resolvePath(helpersRoot, 'dist', `${helper}.js`), 'utf-8');
@@ -1,14 +1,14 @@
1
1
  import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
- import { multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
3
2
  import { refToName } from '@amritk/helpers/ref-to-name';
4
3
  import { resolveRef } from '@amritk/helpers/resolve-ref';
5
4
  import { safeAccessor, safeKey } from '@amritk/helpers/safe-accessor';
6
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
5
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasOneOf, hasProperties, hasRef, hasType, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
7
6
  import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
8
7
  import { findDiscriminator } from '../helpers/find-discriminator.js';
9
8
  import { getDefaultValue } from '../helpers/get-default-value.js';
10
9
  import { getDiscriminatorValue } from '../helpers/get-discriminator-value.js';
11
10
  import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
11
+ import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, shapeValidatorName, } from './generate-type-checks.js';
12
12
  import { generateValidationExpression, scalarItemTypeCheck } from './generate-validation-expression.js';
13
13
  /**
14
14
  * Generates the parser function name from a type name.
@@ -374,6 +374,30 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
374
374
  return dispatch;
375
375
  }
376
376
  }
377
+ // An alias definition (a bare `$ref` with no shape of its own, e.g. a root
378
+ // schema that is just `$ref: '#/$defs/expr'`) delegates to the referenced
379
+ // parser instead of blindly casting, in both modes. Guarded against
380
+ // self-reference: delegating to ourselves would recurse forever.
381
+ if (unionCtx?.useRefImports && isSchemaObject(schema) && hasRef(schema) && !hasProperties(schema)) {
382
+ const refName = refToName(schema.$ref, unionCtx.suffix);
383
+ if (refName !== typeName) {
384
+ return `export const ${functionName} = (input: unknown): ${typeName} => ${generateParserName(refName)}(input) as ${typeName};`;
385
+ }
386
+ }
387
+ // Strict union enforcement for inline (or mixed) branches: membership is the
388
+ // disjunction of per-branch checks, and a non-member throws. Only emitted
389
+ // when every branch check is false-sound (see canEnforceUnion) — otherwise a
390
+ // conservative stub validator somewhere in the graph could reject valid
391
+ // input — and never when stripping unknown keys (see UnionParserContext).
392
+ if (strict && unionCtx && isSchemaObject(schema)) {
393
+ const branches = getUnionBranches(schema);
394
+ if (branches && !unionCtx.stripUnknown && canEnforceUnion(branches, unionCtx.rootSchema)) {
395
+ const check = generateUnionCheck('input', branches, unionCtx.useRefImports, unionCtx.suffix);
396
+ if (check !== null) {
397
+ return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!(${check})) throw new Error(${JSON.stringify(`[${typeName}] value does not match any union branch`)});\n return input as ${typeName};\n};`;
398
+ }
399
+ }
400
+ }
377
401
  // `const`/`enum` define the value space directly (the generated type is a
378
402
  // literal / literal-union), so a non-member must coerce to a valid member —
379
403
  // otherwise the returned value would not be of the declared type. This runs
@@ -471,12 +495,6 @@ const toVarName = (key) => {
471
495
  const safe = key.replace(/[^a-zA-Z0-9_$]/g, '_');
472
496
  return `_${safe}`;
473
497
  };
474
- /**
475
- * Returns the shape-predicate function name for a given type name.
476
- * The predicate is generated alongside each parser and tells callers
477
- * whether `input` is already in the shape produced by the parser's fast path.
478
- */
479
- const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
480
498
  /**
481
499
  * Matches an inline nested object property — an object schema written directly
482
500
  * under `properties` with its own `properties`, rather than referenced via
@@ -544,119 +562,6 @@ const hasStrictKeys = (schema) => {
544
562
  return false;
545
563
  return true;
546
564
  };
547
- /**
548
- * Generates a fast-path type check expression for a property.
549
- * Returns null if the schema is too complex for a simple type check
550
- * (e.g. has unions, enums that require more elaborate validation).
551
- *
552
- * When `useRefImports` is true, $ref properties (and arrays of $refs) are
553
- * checked by calling the imported shape predicate, allowing parent parsers
554
- * to fast-path through nested ref types without recursing into their parsers.
555
- */
556
- const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
557
- if (!isSchemaObject(schema))
558
- return null;
559
- // $ref via shape predicate (deep fast-path through nested types).
560
- if (useRefImports && hasRef(schema)) {
561
- const refName = refToName(schema.$ref, suffix);
562
- return `${shapeValidatorName(refName)}(${varName})`;
563
- }
564
- // Skip fast path for complex schemas that need more elaborate validation
565
- if (hasEnum(schema) ||
566
- hasOneOf(schema) ||
567
- hasAnyOf(schema) ||
568
- hasAllOf(schema) ||
569
- hasRef(schema) ||
570
- 'not' in schema) {
571
- return null;
572
- }
573
- // A `const` property (the usual discriminated-union tag, e.g. `kind: "lit"`) has
574
- // no `type`, but is fast-path predicable via strict equality. Handling it here is
575
- // what lets a discriminated branch's shape validator be a real predicate instead
576
- // of the `=> false` stub. Structural consts need deep equality, so skip those.
577
- if (hasConst(schema)) {
578
- const c = schema.const;
579
- return c === null || typeof c !== 'object' ? `${varName} === ${JSON.stringify(c)}` : null;
580
- }
581
- if (!hasType(schema))
582
- return null;
583
- // Array of $refs: each item must satisfy the imported shape predicate.
584
- if (useRefImports &&
585
- schema.type === 'array' &&
586
- hasItems(schema) &&
587
- isSchemaObject(schema.items) &&
588
- hasRef(schema.items)) {
589
- const refName = refToName(schema.items.$ref, suffix);
590
- return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`;
591
- }
592
- const checks = [];
593
- switch (schema.type) {
594
- case 'string': {
595
- checks.push(`typeof ${varName} === "string"`);
596
- if (hasPattern(schema))
597
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
598
- if (hasMinLength(schema))
599
- checks.push(`${varName}.length >= ${schema.minLength}`);
600
- if (hasMaxLength(schema))
601
- checks.push(`${varName}.length <= ${schema.maxLength}`);
602
- break;
603
- }
604
- case 'number':
605
- case 'integer': {
606
- checks.push(`typeof ${varName} === "number"`);
607
- // `integer` must reject non-integral numbers on the fast path too, otherwise
608
- // `1.5` would pass through uncoerced.
609
- if (schema.type === 'integer')
610
- checks.push(`Number.isInteger(${varName})`);
611
- if (hasMinimum(schema))
612
- checks.push(`${varName} >= ${schema.minimum}`);
613
- if (hasMaximum(schema))
614
- checks.push(`${varName} <= ${schema.maximum}`);
615
- if (hasExclusiveMinimum(schema))
616
- checks.push(`${varName} > ${schema.exclusiveMinimum}`);
617
- if (hasExclusiveMaximum(schema))
618
- checks.push(`${varName} < ${schema.exclusiveMaximum}`);
619
- if (hasMultipleOf(schema))
620
- checks.push(multipleOfPassExpr(varName, schema.multipleOf));
621
- break;
622
- }
623
- case 'boolean':
624
- checks.push(`typeof ${varName} === "boolean"`);
625
- break;
626
- case 'array': {
627
- checks.push(`Array.isArray(${varName})`);
628
- if (hasMinItems(schema))
629
- checks.push(`${varName}.length >= ${schema.minItems}`);
630
- if (hasMaxItems(schema))
631
- checks.push(`${varName}.length <= ${schema.maxItems}`);
632
- if (hasUniqueItems(schema) && schema.uniqueItems === true) {
633
- checks.push(`new Set(${varName}).size === ${varName}.length`);
634
- }
635
- // For a scalar item type, every element must already be well-typed to take
636
- // the fast path; a mistyped element routes the array to the slow path where
637
- // each element is coerced.
638
- if (hasItems(schema) && !Array.isArray(schema.items)) {
639
- const itemCheck = scalarItemTypeCheck(schema.items, '_it');
640
- if (itemCheck)
641
- checks.push(`${varName}.every((_it) => ${itemCheck})`);
642
- }
643
- break;
644
- }
645
- case 'object':
646
- checks.push(`isObject(${varName})`);
647
- break;
648
- default:
649
- return null;
650
- }
651
- if (checks.length === 0)
652
- return null;
653
- // checks[0] is safe: we guard with checks.length === 0 above
654
- let result = checks[0];
655
- for (let i = 1; i < checks.length; i++) {
656
- result += ' && ' + checks[i];
657
- }
658
- return result;
659
- };
660
565
  /**
661
566
  * Determines if a property needs a local variable or can be inlined.
662
567
  * Schema-object properties are always cached: the slow-path ternary chain
@@ -677,7 +582,7 @@ const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
677
582
  * - Fast path that returns input directly when all properties are already valid
678
583
  * - Slow path with optimized expressions
679
584
  */
680
- const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false) => {
585
+ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema) => {
681
586
  const functionName = generateParserName(typeName);
682
587
  const exportPrefix = exported ? 'export ' : '';
683
588
  if (!hasProperties(schema)) {
@@ -697,7 +602,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
697
602
  const propSchema = schema.properties[key];
698
603
  preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
699
604
  preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown));
700
- preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown));
605
+ preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema));
701
606
  }
702
607
  // additionalProperties: false — the per-key "is this undeclared" test inlines
703
608
  // `!==` comparisons for a short key list and hoists a Set only for a long one
@@ -995,7 +900,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
995
900
  lines.push(...varDeclLines);
996
901
  lines.push(...warnLines);
997
902
  // The first assertion line is the `isObject` check, already done above.
998
- const assertionLines = generateObjectStrictAssertion(schema, typeName).slice(1);
903
+ const assertionLines = generateObjectStrictAssertion(schema, typeName, {
904
+ useRefImports,
905
+ suffix,
906
+ stripUnknown,
907
+ ...(rootSchema !== undefined ? { rootSchema } : {}),
908
+ }).slice(1);
999
909
  if (shallowGuard) {
1000
910
  // stripUnknown: a well-typed input skips the assertions and goes straight
1001
911
  // to the strip build (which removes extras and recurses into sub-parsers).
@@ -1436,6 +1346,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1436
1346
  return generateNonObjectParser(typeName, schema, strict, {
1437
1347
  useRefImports,
1438
1348
  suffix,
1349
+ stripUnknown,
1439
1350
  ...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
1440
1351
  });
1441
1352
  }
@@ -1450,7 +1361,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1450
1361
  // properties AND conditional keywords use all declared properties rather than
1451
1362
  // only the if/then fragment.
1452
1363
  if (hasProperties(schema)) {
1453
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
1364
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema);
1454
1365
  }
1455
1366
  // Handle conditional schemas (if/then/else) for schemas without explicit properties.
1456
1367
  if (isSchemaObject(schema) && 'if' in schema && 'then' in schema && 'else' in schema) {
@@ -1467,6 +1378,7 @@ const selectParserStrategy = (schema, typeName, options) => {
1467
1378
  return generateNonObjectParser(typeName, schema, strict, {
1468
1379
  useRefImports,
1469
1380
  suffix,
1381
+ stripUnknown,
1470
1382
  ...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
1471
1383
  });
1472
1384
  }
@@ -1527,6 +1439,31 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
1527
1439
  const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
1528
1440
  if (!isSchemaObject(schema))
1529
1441
  return stub;
1442
+ // An alias definition (a bare `$ref` with no shape of its own) delegates to
1443
+ // the referenced type's validator. Guarded against self-reference.
1444
+ if (useRefImports && hasRef(schema) && !hasProperties(schema)) {
1445
+ const refName = refToName(schema.$ref, suffix);
1446
+ if (refName !== typeName) {
1447
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${shapeValidatorName(refName)}(input);`;
1448
+ }
1449
+ return stub;
1450
+ }
1451
+ // A pure union definition (file-level oneOf/anyOf without properties) gets a
1452
+ // real membership predicate when every branch is checkable. A recursive
1453
+ // union (e.g. `expr` whose branches $ref `expr` itself) works because the
1454
+ // branch checks call this very validator by name at runtime. Skipped under
1455
+ // stripUnknown: the branch checks carry no known-keys terms, so a `true`
1456
+ // could not guarantee the parser's strip build would be a no-op.
1457
+ if (!hasProperties(schema) && !('patternProperties' in schema) && !('if' in schema) && !stripUnknown) {
1458
+ const branches = getUnionBranches(schema);
1459
+ if (branches) {
1460
+ const check = generateUnionCheck('input', branches, useRefImports, suffix);
1461
+ if (check !== null) {
1462
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${check};`;
1463
+ }
1464
+ }
1465
+ return stub;
1466
+ }
1530
1467
  if (!hasProperties(schema))
1531
1468
  return stub;
1532
1469
  // Composition / conditional schemas can match in many shapes — bail.
@@ -1,4 +1,19 @@
1
1
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Context for assertions that reach beyond the property's own schema: union
4
+ * membership checks may call imported `validate{X}Shape` predicates ($ref
5
+ * branches), which requires knowing the ref-import mode, the type-name suffix,
6
+ * and the root document (to prove those validators are real — see
7
+ * canEnforceUnion). `stripUnknown` disables union enforcement entirely, since
8
+ * shape validators then treat undeclared keys as a mismatch while the
9
+ * stripUnknown contract is to drop them.
10
+ */
11
+ export type StrictAssertionContext = {
12
+ readonly useRefImports?: boolean;
13
+ readonly suffix?: string;
14
+ readonly rootSchema?: Record<string, unknown>;
15
+ readonly stripUnknown?: boolean;
16
+ };
2
17
  /**
3
18
  * Generates strict-mode assertions for the body of an object parser.
4
19
  * Throws on:
@@ -10,7 +25,7 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
10
25
  * Properties with a `$ref` are validated by the nested parser's own strict
11
26
  * check when that parser is invoked downstream.
12
27
  */
13
- export declare const generateObjectStrictAssertion: (schema: JSONSchema, typeName: string) => string[];
28
+ export declare const generateObjectStrictAssertion: (schema: JSONSchema, typeName: string, context?: StrictAssertionContext) => string[];
14
29
  /**
15
30
  * Generates a single strict-mode assertion line for a non-object scalar parser.
16
31
  * Returns null when the schema has no type information to assert on.
@@ -2,7 +2,10 @@ import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
2
  import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
3
3
  import { multipleOfFailExpr } from '@amritk/helpers/multiple-of-check';
4
4
  import { safeAccessor } from '@amritk/helpers/safe-accessor';
5
- import { hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
5
+ import { hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
6
+ import { generateEnumCheck } from './generate-enum-check.js';
7
+ import { canEnforceUnion, generateUnionCheck, getUnionBranches } from './generate-type-checks.js';
8
+ import { scalarItemTypeCheck } from './generate-validation-expression.js';
6
9
  /**
7
10
  * Returns the inline condition that is true when `accessor` is the wrong type
8
11
  * for the given JSON Schema primitive type.
@@ -94,15 +97,41 @@ const generateConstraintChecks = (acc, propSchema, typeName, key) => {
94
97
  // a plain `new Set` would only catch primitive duplicates, not equal objects.
95
98
  lines.push(` if (Array.isArray(${acc}) && new Set(${acc}.map((_u) => JSON.stringify(_u))).size !== ${acc}.length) ${throwError(`${field} must NOT have duplicate items`)};`);
96
99
  }
100
+ // Item types: the fast path proves them via `.every`, but this slow path
101
+ // used to check only length/uniqueness, letting e.g. a number slip into a
102
+ // declared `string[]`. Enforce scalar and enum item schemas here; richer
103
+ // item schemas ($refs, objects) are validated by their own parsers.
104
+ const itemCheck = generateItemCheck(propSchema);
105
+ if (itemCheck) {
106
+ lines.push(` if (Array.isArray(${acc}) && !${acc}.every((_it) => ${itemCheck.check})) ${throwError(`${field} ${itemCheck.message}`)};`);
107
+ }
97
108
  }
98
109
  return lines;
99
110
  };
111
+ /**
112
+ * Boolean per-item check (bound to `_it`) for an array schema's `items`, with
113
+ * the error-message fragment describing what was expected. Only scalar types
114
+ * and enums are checked — returns null for anything richer.
115
+ */
116
+ const generateItemCheck = (schema) => {
117
+ if (!isSchemaObject(schema) || !hasItems(schema) || Array.isArray(schema.items))
118
+ return null;
119
+ const items = schema.items;
120
+ if (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0) {
121
+ const label = items.enum.map((v) => JSON.stringify(v)).join(', ');
122
+ return { check: generateEnumCheck('_it', items.enum), message: `items must be one of: ${label}` };
123
+ }
124
+ const scalarCheck = scalarItemTypeCheck(items, '_it');
125
+ if (scalarCheck === null || !isSchemaObject(items) || !hasType(items))
126
+ return null;
127
+ return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
128
+ };
100
129
  /**
101
130
  * Generates strict-mode lines for a single property of an object schema.
102
131
  * Properties with a `$ref` are skipped here — the nested parser handles its own
103
132
  * strict check when called from the parent's slow path.
104
133
  */
105
- const generatePropertyAssertion = (key, propSchema, isRequired, typeName) => {
134
+ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, context = {}) => {
106
135
  const acc = safeAccessor('input', key);
107
136
  const field = `[${typeName}] field '${key}'`;
108
137
  const lines = [];
@@ -113,6 +142,21 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName) => {
113
142
  return lines;
114
143
  if (hasRef(propSchema))
115
144
  return lines;
145
+ // Union properties: enforce membership when every branch check is
146
+ // false-sound (canEnforceUnion), so a value matching no variant throws
147
+ // instead of passing through untyped. Left unenforced (pass-through, the
148
+ // historical behavior) when any branch is too complex to check safely.
149
+ const unionBranches = getUnionBranches(propSchema);
150
+ if (unionBranches) {
151
+ if (!context.stripUnknown && canEnforceUnion(unionBranches, context.rootSchema)) {
152
+ const check = generateUnionCheck(acc, unionBranches, context.useRefImports ?? false, context.suffix ?? '');
153
+ if (check !== null) {
154
+ const failure = throwError(`${field} does not match any allowed variant`);
155
+ lines.push(isRequired ? ` if (!(${check})) ${failure};` : ` if (${acc} !== undefined && !(${check})) ${failure};`);
156
+ }
157
+ }
158
+ return lines;
159
+ }
116
160
  const instanceOf = getMjstInstanceOf(propSchema);
117
161
  if (instanceOf) {
118
162
  if (isRequired) {
@@ -171,14 +215,14 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName) => {
171
215
  * Properties with a `$ref` are validated by the nested parser's own strict
172
216
  * check when that parser is invoked downstream.
173
217
  */
174
- export const generateObjectStrictAssertion = (schema, typeName) => {
218
+ export const generateObjectStrictAssertion = (schema, typeName, context = {}) => {
175
219
  const lines = [];
176
220
  lines.push(` if (!isObject(input)) ${throwError(`[${typeName}] expected object, got `, 'input === null ? "null" : typeof input')};`);
177
221
  if (!hasProperties(schema) || !isSchemaObject(schema))
178
222
  return lines;
179
223
  const required = new Set(hasRequired(schema) ? schema.required : []);
180
224
  for (const [key, propSchema] of Object.entries(schema.properties)) {
181
- lines.push(...generatePropertyAssertion(key, propSchema, required.has(key), typeName));
225
+ lines.push(...generatePropertyAssertion(key, propSchema, required.has(key), typeName, context));
182
226
  }
183
227
  return lines;
184
228
  };
@@ -202,5 +246,14 @@ export const generateScalarStrictAssertion = (schema, typeName) => {
202
246
  const wrongType = wrongTypeCondition('input', t);
203
247
  if (!wrongType)
204
248
  return null;
205
- return ` if (${wrongType}) ${throwError(`[${typeName}] expected ${typeLabel(t)}, got `, got)};`;
249
+ const lines = [` if (${wrongType}) ${throwError(`[${typeName}] expected ${typeLabel(t)}, got `, got)};`];
250
+ // Root-level arrays enforce scalar/enum item types too — the same gap the
251
+ // property path closes in generateConstraintChecks.
252
+ if (t === 'array') {
253
+ const itemCheck = generateItemCheck(schema);
254
+ if (itemCheck) {
255
+ lines.push(` if (!(input as readonly unknown[]).every((_it) => ${itemCheck.check})) ${throwError(`[${typeName}] ${itemCheck.message}`)};`);
256
+ }
257
+ }
258
+ return lines.join('\n');
206
259
  };
@@ -0,0 +1,65 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Boolean type-check expression builders shared by the shape validators, the
4
+ * parsers' fast paths, and the strict-mode assertions. They live in their own
5
+ * module (rather than generate-parser-function) so generate-strict-assertion
6
+ * can use them without an import cycle.
7
+ *
8
+ * Every expression built here is *true-sound*: when it evaluates to true the
9
+ * value definitely matches the schema fragment. That is all a fast path needs.
10
+ * The reverse direction — false definitely means invalid — only holds when
11
+ * {@link canEnforceUnion} approves the schema, because a check may call an
12
+ * imported `validate{X}Shape` that was generated as a conservative `=> false`
13
+ * stub. Strict-mode code that *throws* on a false check must gate on it.
14
+ */
15
+ /**
16
+ * Returns the shape-predicate function name for a given type name.
17
+ * The predicate is generated alongside each parser and tells callers
18
+ * whether `input` is already in the shape produced by the parser's fast path.
19
+ */
20
+ export declare const shapeValidatorName: (typeName: string) => string;
21
+ /**
22
+ * Extracts the branch list of a `oneOf`/`anyOf` union, or `null` when the
23
+ * schema is not a union (or mixes in other composition keywords we cannot
24
+ * turn into a membership check).
25
+ */
26
+ export declare const getUnionBranches: (schema: JSONSchema) => readonly JSONSchema[] | null;
27
+ /**
28
+ * Generates a fast-path type check expression for a property.
29
+ * Returns null if the schema is too complex for a simple type check.
30
+ *
31
+ * When `useRefImports` is true, $ref properties (and arrays of $refs) are
32
+ * checked by calling the imported shape predicate, allowing parent parsers
33
+ * to fast-path through nested ref types without recursing into their parsers.
34
+ */
35
+ export declare const generatePropertyTypeCheck: (varName: string, schema: JSONSchema, useRefImports: boolean, suffix: string) => string | null;
36
+ /**
37
+ * Generates a boolean check that `varName` matches an inline object schema —
38
+ * `isObject` plus a per-property check for every declared property (and a
39
+ * presence check for `required` keys without a declared schema). Used for
40
+ * union branches written inline, where no named shape validator exists to
41
+ * call. Returns null when any property is too complex to check.
42
+ */
43
+ export declare const generateInlineObjectCheck: (varName: string, schema: JSONSchema, useRefImports: boolean, suffix: string) => string | null;
44
+ /**
45
+ * Generates a union membership check: true when `varName` matches at least one
46
+ * branch of a `oneOf`/`anyOf`. Inline object branches get a per-property check;
47
+ * everything else goes through {@link generatePropertyTypeCheck} ($refs call
48
+ * the imported shape validator, consts/enums/scalars check directly). Returns
49
+ * null when any branch cannot be checked.
50
+ */
51
+ export declare const generateUnionCheck: (varName: string, branches: readonly JSONSchema[], useRefImports: boolean, suffix: string) => string | null;
52
+ /**
53
+ * True when the union membership check emitted by {@link generateUnionCheck}
54
+ * is also *false-sound* — a false result definitely means the value is invalid
55
+ * — so strict mode may throw on it.
56
+ *
57
+ * The risk is `$ref`s: a branch (or a property inside one) whose check calls
58
+ * an imported `validate{X}Shape` is only trustworthy when that validator was
59
+ * generated as a real predicate. This walks every reachable ref (mirroring
60
+ * generateShapeValidator's stub conditions) and rejects the union when any of
61
+ * them would have produced the conservative `=> false` stub, or is itself
62
+ * built on one. Cycles (e.g. a recursive `expr` union) are assumed sound —
63
+ * the recursion bottoms out on the data, not the schema.
64
+ */
65
+ export declare const canEnforceUnion: (branches: readonly JSONSchema[], rootSchema: Record<string, unknown> | undefined) => boolean;
@@ -0,0 +1,370 @@
1
+ import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
+ import { multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
3
+ import { refToName } from '@amritk/helpers/ref-to-name';
4
+ import { resolveRef } from '@amritk/helpers/resolve-ref';
5
+ import { safeAccessor } from '@amritk/helpers/safe-accessor';
6
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
7
+ import { generateEnumCheck } from './generate-enum-check.js';
8
+ import { scalarItemTypeCheck } from './generate-validation-expression.js';
9
+ /**
10
+ * Boolean type-check expression builders shared by the shape validators, the
11
+ * parsers' fast paths, and the strict-mode assertions. They live in their own
12
+ * module (rather than generate-parser-function) so generate-strict-assertion
13
+ * can use them without an import cycle.
14
+ *
15
+ * Every expression built here is *true-sound*: when it evaluates to true the
16
+ * value definitely matches the schema fragment. That is all a fast path needs.
17
+ * The reverse direction — false definitely means invalid — only holds when
18
+ * {@link canEnforceUnion} approves the schema, because a check may call an
19
+ * imported `validate{X}Shape` that was generated as a conservative `=> false`
20
+ * stub. Strict-mode code that *throws* on a false check must gate on it.
21
+ */
22
+ /**
23
+ * Returns the shape-predicate function name for a given type name.
24
+ * The predicate is generated alongside each parser and tells callers
25
+ * whether `input` is already in the shape produced by the parser's fast path.
26
+ */
27
+ export const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
28
+ /**
29
+ * Extracts the branch list of a `oneOf`/`anyOf` union, or `null` when the
30
+ * schema is not a union (or mixes in other composition keywords we cannot
31
+ * turn into a membership check).
32
+ */
33
+ export const getUnionBranches = (schema) => {
34
+ if (!isSchemaObject(schema))
35
+ return null;
36
+ if (hasAllOf(schema) || 'not' in schema)
37
+ return null;
38
+ if (hasOneOf(schema) && schema.oneOf.length > 0)
39
+ return schema.oneOf;
40
+ if (hasAnyOf(schema) && schema.anyOf.length > 0)
41
+ return schema.anyOf;
42
+ return null;
43
+ };
44
+ /**
45
+ * Generates a fast-path type check expression for a property.
46
+ * Returns null if the schema is too complex for a simple type check.
47
+ *
48
+ * When `useRefImports` is true, $ref properties (and arrays of $refs) are
49
+ * checked by calling the imported shape predicate, allowing parent parsers
50
+ * to fast-path through nested ref types without recursing into their parsers.
51
+ */
52
+ export const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
53
+ if (!isSchemaObject(schema))
54
+ return null;
55
+ // $ref via shape predicate (deep fast-path through nested types).
56
+ if (useRefImports && hasRef(schema)) {
57
+ const refName = refToName(schema.$ref, suffix);
58
+ return `${shapeValidatorName(refName)}(${varName})`;
59
+ }
60
+ if (hasRef(schema))
61
+ return null;
62
+ // A fixed member set is directly predicable regardless of `type` — this is
63
+ // what lets a nested object carrying an enum property (e.g. `axiom.kind`)
64
+ // keep a real shape validator instead of the `=> false` stub.
65
+ if (hasEnum(schema)) {
66
+ return schema.enum.length > 0 ? generateEnumCheck(varName, schema.enum) : null;
67
+ }
68
+ // A union property is predicable when every branch is: membership is the
69
+ // disjunction of the branch checks.
70
+ const branches = getUnionBranches(schema);
71
+ if (branches) {
72
+ return generateUnionCheck(varName, branches, useRefImports, suffix);
73
+ }
74
+ // Remaining composition (allOf, not, or a union mixed with them) cannot be
75
+ // expressed as a simple boolean check.
76
+ if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || 'not' in schema) {
77
+ return null;
78
+ }
79
+ // A `const` property (the usual discriminated-union tag, e.g. `kind: "lit"`) has
80
+ // no `type`, but is fast-path predicable via strict equality. Handling it here is
81
+ // what lets a discriminated branch's shape validator be a real predicate instead
82
+ // of the `=> false` stub. Structural consts need deep equality, so skip those.
83
+ if (hasConst(schema)) {
84
+ const c = schema.const;
85
+ return c === null || typeof c !== 'object' ? `${varName} === ${JSON.stringify(c)}` : null;
86
+ }
87
+ if (!hasType(schema))
88
+ return null;
89
+ // Array of $refs: each item must satisfy the imported shape predicate.
90
+ if (useRefImports &&
91
+ schema.type === 'array' &&
92
+ hasItems(schema) &&
93
+ isSchemaObject(schema.items) &&
94
+ hasRef(schema.items)) {
95
+ const refName = refToName(schema.items.$ref, suffix);
96
+ return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`;
97
+ }
98
+ const checks = [];
99
+ switch (schema.type) {
100
+ case 'string': {
101
+ checks.push(`typeof ${varName} === "string"`);
102
+ if (hasPattern(schema))
103
+ checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
104
+ if (hasMinLength(schema))
105
+ checks.push(`${varName}.length >= ${schema.minLength}`);
106
+ if (hasMaxLength(schema))
107
+ checks.push(`${varName}.length <= ${schema.maxLength}`);
108
+ break;
109
+ }
110
+ case 'number':
111
+ case 'integer': {
112
+ checks.push(`typeof ${varName} === "number"`);
113
+ // `integer` must reject non-integral numbers on the fast path too, otherwise
114
+ // `1.5` would pass through uncoerced.
115
+ if (schema.type === 'integer')
116
+ checks.push(`Number.isInteger(${varName})`);
117
+ if (hasMinimum(schema))
118
+ checks.push(`${varName} >= ${schema.minimum}`);
119
+ if (hasMaximum(schema))
120
+ checks.push(`${varName} <= ${schema.maximum}`);
121
+ if (hasExclusiveMinimum(schema))
122
+ checks.push(`${varName} > ${schema.exclusiveMinimum}`);
123
+ if (hasExclusiveMaximum(schema))
124
+ checks.push(`${varName} < ${schema.exclusiveMaximum}`);
125
+ if (hasMultipleOf(schema))
126
+ checks.push(multipleOfPassExpr(varName, schema.multipleOf));
127
+ break;
128
+ }
129
+ case 'boolean':
130
+ checks.push(`typeof ${varName} === "boolean"`);
131
+ break;
132
+ case 'array': {
133
+ checks.push(`Array.isArray(${varName})`);
134
+ if (hasMinItems(schema))
135
+ checks.push(`${varName}.length >= ${schema.minItems}`);
136
+ if (hasMaxItems(schema))
137
+ checks.push(`${varName}.length <= ${schema.maxItems}`);
138
+ if (hasUniqueItems(schema) && schema.uniqueItems === true) {
139
+ checks.push(`new Set(${varName}).size === ${varName}.length`);
140
+ }
141
+ // For a scalar or enum item type, every element must already be well-typed
142
+ // to take the fast path; a mismatched element routes the array to the slow
143
+ // path where each element is coerced (or, in strict mode, throws).
144
+ if (hasItems(schema) && !Array.isArray(schema.items)) {
145
+ const items = schema.items;
146
+ const itemCheck = scalarItemTypeCheck(items, '_it') ??
147
+ (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0
148
+ ? generateEnumCheck('_it', items.enum)
149
+ : null);
150
+ if (itemCheck)
151
+ checks.push(`${varName}.every((_it) => ${itemCheck})`);
152
+ }
153
+ break;
154
+ }
155
+ case 'object':
156
+ checks.push(`isObject(${varName})`);
157
+ break;
158
+ default:
159
+ return null;
160
+ }
161
+ if (checks.length === 0)
162
+ return null;
163
+ // checks[0] is safe: we guard with checks.length === 0 above
164
+ let result = checks[0];
165
+ for (let i = 1; i < checks.length; i++) {
166
+ result += ' && ' + checks[i];
167
+ }
168
+ return result;
169
+ };
170
+ /**
171
+ * Generates a boolean check that `varName` matches an inline object schema —
172
+ * `isObject` plus a per-property check for every declared property (and a
173
+ * presence check for `required` keys without a declared schema). Used for
174
+ * union branches written inline, where no named shape validator exists to
175
+ * call. Returns null when any property is too complex to check.
176
+ */
177
+ export const generateInlineObjectCheck = (varName, schema, useRefImports, suffix) => {
178
+ if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
179
+ return null;
180
+ if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || 'not' in schema)
181
+ return null;
182
+ if ('patternProperties' in schema || 'if' in schema)
183
+ return null;
184
+ // additionalProperties-as-schema (records) would leave the record values
185
+ // unchecked, breaking the "true means valid" guarantee.
186
+ if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== 'boolean')
187
+ return null;
188
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
189
+ const checks = [`isObject(${varName})`];
190
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
191
+ const acc = safeAccessor(varName, key);
192
+ const check = generatePropertyTypeCheck(acc, propSchema, useRefImports, suffix);
193
+ if (check === null)
194
+ return null;
195
+ checks.push(required.has(key) ? check : `(${acc} === undefined || ${check})`);
196
+ required.delete(key);
197
+ }
198
+ // `required` keys without a declared property schema still need a presence check.
199
+ for (const key of required) {
200
+ checks.push(`${JSON.stringify(key)} in ${varName}`);
201
+ }
202
+ // additionalProperties: false — extras make the value invalid for this branch.
203
+ if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
204
+ const allowedKeys = JSON.stringify(Object.keys(schema.properties));
205
+ checks.push(`Object.keys(${varName}).every((_k) => ${allowedKeys}.includes(_k))`);
206
+ }
207
+ return checks.join(' && ');
208
+ };
209
+ /**
210
+ * Generates a union membership check: true when `varName` matches at least one
211
+ * branch of a `oneOf`/`anyOf`. Inline object branches get a per-property check;
212
+ * everything else goes through {@link generatePropertyTypeCheck} ($refs call
213
+ * the imported shape validator, consts/enums/scalars check directly). Returns
214
+ * null when any branch cannot be checked.
215
+ */
216
+ export const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
217
+ if (branches.length === 0)
218
+ return null;
219
+ const parts = [];
220
+ for (const branch of branches) {
221
+ if (!isSchemaObject(branch))
222
+ return null;
223
+ const check = isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
224
+ ? generateInlineObjectCheck(varName, branch, useRefImports, suffix)
225
+ : generatePropertyTypeCheck(varName, branch, useRefImports, suffix);
226
+ if (check === null)
227
+ return null;
228
+ parts.push(`(${check})`);
229
+ }
230
+ // Callers conjoin this with other checks, so a multi-branch disjunction must
231
+ // carry its own parentheses — `a || b && c` binds the wrong way without them.
232
+ return parts.length === 1 ? parts[0] : `(${parts.join(' || ')})`;
233
+ };
234
+ /**
235
+ * True when the union membership check emitted by {@link generateUnionCheck}
236
+ * is also *false-sound* — a false result definitely means the value is invalid
237
+ * — so strict mode may throw on it.
238
+ *
239
+ * The risk is `$ref`s: a branch (or a property inside one) whose check calls
240
+ * an imported `validate{X}Shape` is only trustworthy when that validator was
241
+ * generated as a real predicate. This walks every reachable ref (mirroring
242
+ * generateShapeValidator's stub conditions) and rejects the union when any of
243
+ * them would have produced the conservative `=> false` stub, or is itself
244
+ * built on one. Cycles (e.g. a recursive `expr` union) are assumed sound —
245
+ * the recursion bottoms out on the data, not the schema.
246
+ */
247
+ export const canEnforceUnion = (branches, rootSchema) => {
248
+ const visiting = new Set();
249
+ return (branches.length > 0 &&
250
+ branches.every((branch) => {
251
+ if (!isSchemaObject(branch))
252
+ return false;
253
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
254
+ ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
255
+ : canTrustPropertyCheck(branch, rootSchema, visiting);
256
+ }));
257
+ };
258
+ /** Mirrors {@link generatePropertyTypeCheck}: non-null AND every embedded validator call is real. */
259
+ const canTrustPropertyCheck = (schema, rootSchema, visiting) => {
260
+ if (!isSchemaObject(schema))
261
+ return false;
262
+ if (hasRef(schema)) {
263
+ return canTrustReferencedValidator(schema.$ref, rootSchema, visiting);
264
+ }
265
+ if (hasEnum(schema))
266
+ return schema.enum.length > 0;
267
+ const branches = getUnionBranches(schema);
268
+ if (branches) {
269
+ return branches.every((branch) => {
270
+ if (!isSchemaObject(branch))
271
+ return false;
272
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
273
+ ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
274
+ : canTrustPropertyCheck(branch, rootSchema, visiting);
275
+ });
276
+ }
277
+ if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || 'not' in schema)
278
+ return false;
279
+ if (hasConst(schema))
280
+ return schema.const === null || typeof schema.const !== 'object';
281
+ if (!hasType(schema))
282
+ return false;
283
+ if (schema.type === 'array' && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
284
+ return canTrustReferencedValidator(schema.items.$ref, rootSchema, visiting);
285
+ }
286
+ switch (schema.type) {
287
+ case 'string':
288
+ case 'number':
289
+ case 'integer':
290
+ case 'boolean':
291
+ case 'array':
292
+ case 'object':
293
+ return true;
294
+ default:
295
+ return false;
296
+ }
297
+ };
298
+ /** Mirrors {@link generateInlineObjectCheck}: non-null AND every property check is trustworthy. */
299
+ const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
300
+ if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
301
+ return false;
302
+ if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || 'not' in schema)
303
+ return false;
304
+ if ('patternProperties' in schema || 'if' in schema)
305
+ return false;
306
+ if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== 'boolean')
307
+ return false;
308
+ return Object.values(schema.properties).every((propSchema) => canTrustPropertyCheck(propSchema, rootSchema, visiting));
309
+ };
310
+ /**
311
+ * True when the file generated for `ref`'s target carries a *real* (non-stub)
312
+ * shape validator whose result can be trusted in both directions. Mirrors the
313
+ * schema classes generateShapeValidator (and its new union/alias paths) turns
314
+ * into real predicates, recursing into every reachable ref.
315
+ */
316
+ const canTrustReferencedValidator = (ref, rootSchema, visiting) => {
317
+ if (visiting.has(ref))
318
+ return true;
319
+ if (!rootSchema)
320
+ return false;
321
+ const resolved = resolveRef(ref, rootSchema);
322
+ if (!resolved || !isSchemaObject(resolved))
323
+ return false;
324
+ visiting.add(ref);
325
+ try {
326
+ // Alias definition (bare $ref): its validator delegates to the target's.
327
+ if (hasRef(resolved) && !hasProperties(resolved)) {
328
+ return canTrustReferencedValidator(resolved.$ref, rootSchema, visiting);
329
+ }
330
+ // Pure union definition: real when the membership check is enforceable.
331
+ if (!hasProperties(resolved)) {
332
+ const branches = getUnionBranches(resolved);
333
+ if (!branches)
334
+ return false;
335
+ if ('patternProperties' in resolved || 'if' in resolved)
336
+ return false;
337
+ return branches.every((branch) => {
338
+ if (!isSchemaObject(branch))
339
+ return false;
340
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
341
+ ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
342
+ : canTrustPropertyCheck(branch, rootSchema, visiting);
343
+ });
344
+ }
345
+ // Object definition: real when generateShapeValidator emits a predicate —
346
+ // no composition/conditional keywords, and every property check resolves.
347
+ if (hasOneOf(resolved) ||
348
+ hasAnyOf(resolved) ||
349
+ hasAllOf(resolved) ||
350
+ 'not' in resolved ||
351
+ 'patternProperties' in resolved ||
352
+ 'if' in resolved ||
353
+ 'then' in resolved ||
354
+ 'else' in resolved) {
355
+ return false;
356
+ }
357
+ return Object.values(resolved.properties).every((propSchema) => {
358
+ // Records of refs make generateShapeValidator bail to the stub.
359
+ if (isSchemaObject(propSchema) &&
360
+ hasAdditionalProperties(propSchema) &&
361
+ typeof propSchema.additionalProperties !== 'boolean') {
362
+ return false;
363
+ }
364
+ return canTrustPropertyCheck(propSchema, rootSchema, visiting);
365
+ });
366
+ }
367
+ finally {
368
+ visiting.delete(ref);
369
+ }
370
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.12.3",
3
+ "version": "0.13.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "json-schema-typed": "^8.0.1",
51
- "@amritk/helpers": "0.10.2",
51
+ "@amritk/helpers": "0.10.3",
52
52
  "@amritk/generate-markdown": "0.4.1"
53
53
  },
54
54
  "devDependencies": {