@amritk/generate-validators 0.10.0 → 0.10.1

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.
@@ -580,7 +580,12 @@ const generateObjectValidator = (schema, typeName, suffix) => {
580
580
  if (hasPropertyNames(schema) && isSchemaObject(schema.propertyNames)) {
581
581
  propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix));
582
582
  }
583
- const body = propertyLines.length > 0 ? '\n' + propertyLines.join('\n') + '\n' : '';
583
+ // Lazily allocate the errors array so a valid input never builds one — the same
584
+ // allocation-free happy path the runtime interpreter uses. Each emitted
585
+ // `errors.push(...)` becomes a create-on-first-use push; nothing is allocated
586
+ // until the first actual error, so the common valid case stays alloc-free even
587
+ // when the schema is too rich for the boolean guard.
588
+ const body = (propertyLines.length > 0 ? '\n' + propertyLines.join('\n') + '\n' : '').replaceAll('errors.push(', '(errors ??= []).push(');
584
589
  // Hoisted statements (e.g. known-keys Sets) come first so every call of the
585
590
  // validator reuses them instead of rebuilding them.
586
591
  const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join('\n')}\n\n` : '';
@@ -602,9 +607,9 @@ const generateObjectValidator = (schema, typeName, suffix) => {
602
607
  ` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
603
608
  ` }`,
604
609
  ``,
605
- ` const errors: ValidationError[] = []`,
610
+ ` let errors: ValidationError[] | undefined`,
606
611
  body,
607
- ` return errors.length > 0 ? { valid: false, errors } : true`,
612
+ ` return errors !== undefined ? { valid: false, errors } : true`,
608
613
  `}`,
609
614
  ].join('\n');
610
615
  // No guard: the exported validator is the error-collecting function itself.
@@ -661,6 +666,23 @@ const rightTypeCondition = (accessor, type) => {
661
666
  return null;
662
667
  }
663
668
  };
669
+ /**
670
+ * Builds the membership test for an `enum`, matching the slow path's
671
+ * `[...].includes(value)` verdict exactly. For the common all-primitive case it
672
+ * emits a parenthesized `a === x || a === y` chain — no per-call array
673
+ * allocation and no linear scan, so it stays on the allocation-free hot path —
674
+ * and falls back to `.includes` when a member is an object/array (reference
675
+ * equality) or `NaN` (where `includes`'s SameValueZero differs from `===`).
676
+ */
677
+ const enumMembershipExpr = (values, acc) => {
678
+ const allPrimitive = values.length > 0 &&
679
+ values.every((v) => (v === null || typeof v !== 'object') && typeof v !== 'function') &&
680
+ !values.some((v) => typeof v === 'number' && Number.isNaN(v));
681
+ if (allPrimitive) {
682
+ return `(${values.map((v) => `${acc} === ${JSON.stringify(v)}`).join(' || ')})`;
683
+ }
684
+ return `(${JSON.stringify(values)} as unknown[]).includes(${acc})`;
685
+ };
664
686
  /**
665
687
  * Builds a boolean expression that is TRUE iff `acc` satisfies `schema`, with the
666
688
  * *exact same verdict* as the error-collecting validator — or `null` when the
@@ -686,7 +708,7 @@ const booleanLeafExpr = (schema, acc) => {
686
708
  }
687
709
  // enum — same membership test the validator uses.
688
710
  if (hasEnum(schema)) {
689
- return `(${JSON.stringify(schema.enum)} as unknown[]).includes(${acc})`;
711
+ return enumMembershipExpr(schema.enum, acc);
690
712
  }
691
713
  if (!hasType(schema))
692
714
  return null;
@@ -738,6 +760,12 @@ const booleanLeafExpr = (schema, acc) => {
738
760
  * array shape and — for typed items — only each item's *type* (objects are shape-
739
761
  * checked, not recursed into); it never enforces `minItems`/`maxItems` or item
740
762
  * constraints. Returns `null` for `$ref` items (those defer to the validator).
763
+ *
764
+ * Item iteration goes through `Array.from` rather than `Array.prototype.every`
765
+ * because `every` *skips holes* in a sparse array (`[, 'x']`), whereas the
766
+ * validator's index-based `for` loop reads a hole as `undefined` and rejects it.
767
+ * Materialising the array first makes the guard's verdict match the slow path's
768
+ * on sparse input — the guard must never accept what the slow path would reject.
741
769
  */
742
770
  const booleanArrayExpr = (schema, acc) => {
743
771
  const base = `Array.isArray(${acc})`;
@@ -753,7 +781,7 @@ const booleanArrayExpr = (schema, acc) => {
753
781
  const itemCheck = rightTypeCondition('_it', items.type);
754
782
  if (itemCheck === null)
755
783
  return base;
756
- return `${base} && (${acc} as unknown[]).every((_it) => ${itemCheck})`;
784
+ return `${base} && Array.from(${acc} as unknown[]).every((_it) => ${itemCheck})`;
757
785
  };
758
786
  /**
759
787
  * Builds the `&&` conditions proving an object value is valid (same verdict as
@@ -978,9 +1006,9 @@ const generateScalarValidator = (schema, typeName, suffix) => {
978
1006
  ` if (${wrongType}) {`,
979
1007
  ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
980
1008
  ` }`,
981
- ` const errors: ValidationError[] = []`,
982
- constraintLines.join('\n'),
983
- ` return errors.length > 0 ? { valid: false, errors } : true`,
1009
+ ` let errors: ValidationError[] | undefined`,
1010
+ constraintLines.join('\n').replaceAll('errors.push(', '(errors ??= []).push('),
1011
+ ` return errors !== undefined ? { valid: false, errors } : true`,
984
1012
  `}`,
985
1013
  ].join('\n');
986
1014
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-validators",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Generate TypeScript validation functions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",