@whatsaaaa/form-builder-schema 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,21 @@
1
1
  import * as v from "valibot";
2
+ // src/errors/definition-error.ts
3
+ function formatPath(path) {
4
+ return path.reduce((out, key) => {
5
+ if (typeof key === "number") return `${out}[${key}]`;
6
+ return out ? `${out}.${key}` : key;
7
+ }, "");
8
+ }
9
+ /** Thrown by `build()` when the definition has issues. Lists all of them. */
10
+ var DefinitionError = class extends Error {
11
+ issues;
12
+ constructor(issues) {
13
+ const lines = issues.map((issue) => ` ${formatPath(issue.path) || "(root)"}: ${issue.message}`);
14
+ super(`Invalid form definition:\n${lines.join("\n")}`);
15
+ this.name = "DefinitionError";
16
+ this.issues = issues;
17
+ }
18
+ };
2
19
  // src/shared/each-item.ts
3
20
  /**
4
21
  * Checks every item of an array with `schema` and reports the issues on the array itself.
@@ -23,9 +40,10 @@ function eachItem(schema) {
23
40
  /**
24
41
  * The schema runs in this order: empty check, base schema, normalize rules, required check,
25
42
  * then the type's default rules and the field's rules, and last the type's `finalize()`.
43
+ * A container has no empty check: its value is never empty.
26
44
  */
27
- function buildFieldSchema(checked, messages) {
28
- const { field, path, type, kind } = checked;
45
+ function buildFieldSchema(checked, messages, ctx) {
46
+ const { field, path, type, kind, valueKind } = checked;
29
47
  const required = !!field.required;
30
48
  const requiredMessage = messages.resolve(typeof field.required === "object" && field.required.message || messages.required, {
31
49
  field,
@@ -54,15 +72,52 @@ function buildFieldSchema(checked, messages) {
54
72
  if (each.validate.length) whole.validate.unshift(eachItem(pipe(v.unknown(), each.validate)));
55
73
  const finalize = type.finalize?.(field);
56
74
  if (finalize) whole.validate.push(finalize);
57
- const base = type.schema(field, { path });
75
+ const base = type.schema(field, ctx);
58
76
  const requiredCheck = !required ? void 0 : type.required ? type.required(requiredMessage) : kind.required?.(requiredMessage);
59
77
  const head = pipe(base, requiredCheck ? [...whole.normalize, requiredCheck] : whole.normalize);
78
+ if (type.container) {
79
+ const rules = whole.validate.length ? [pipe(v.unknown(), whole.validate)] : [];
80
+ return v.optional(pipe(head, rules), valueKind === "object" ? () => ({}) : () => []);
81
+ }
60
82
  const empty = required ? v.never(requiredMessage) : v.pipe(v.unknown(), v.transform(() => void 0));
61
83
  const whenFilled = (schema) => v.lazy((input) => kind.isEmpty(input) ? empty : schema);
62
84
  const schema = whenFilled(v.pipe(head, whenFilled(pipe(v.unknown(), whole.validate))));
63
85
  return required ? schema : v.optional(schema);
64
86
  }
65
87
  const pipe = (schema, items) => items.reduce((acc, item) => v.pipe(acc, item), schema);
88
+ // src/shared/reserved-keys.ts
89
+ /**
90
+ * Field keys this package owns. A field type's props can't use them.
91
+ */
92
+ const RESERVED_FIELD_KEYS = [
93
+ "name",
94
+ "type",
95
+ "label",
96
+ "description",
97
+ "placeholder",
98
+ "default",
99
+ "required",
100
+ "rules",
101
+ "ui"
102
+ ];
103
+ /**
104
+ * Rule keys this package owns. A rule's params can't use them.
105
+ */
106
+ const RESERVED_RULE_KEYS = ["rule", "message"];
107
+ /**
108
+ * Form rule keys this package owns. A form rule's params can't use them.
109
+ */
110
+ const RESERVED_FORM_RULE_KEYS = [
111
+ "rule",
112
+ "field",
113
+ "message"
114
+ ];
115
+ const fieldKeys = new Set(RESERVED_FIELD_KEYS);
116
+ const ruleKeys = new Set(RESERVED_RULE_KEYS);
117
+ const formRuleKeys = new Set(RESERVED_FORM_RULE_KEYS);
118
+ const isReservedFieldKey = (key) => fieldKeys.has(key);
119
+ const isReservedRuleKey = (key) => ruleKeys.has(key);
120
+ const isReservedFormRuleKey = (key) => formRuleKeys.has(key);
66
121
  // src/field-types/basic-types.ts
67
122
  /** Types with one plain value and no props. Each maps to a native input. */
68
123
  const basicTypes = {
@@ -147,6 +202,26 @@ const choiceTypes = {
147
202
  schema: (field) => v.array(oneOf(field.options))
148
203
  })
149
204
  };
205
+ // src/field-types/container-types.ts
206
+ const childField = v.custom(() => true);
207
+ /** Types whose value holds other fields. Display elements can go inside them too. */
208
+ const containerTypes = {
209
+ group: defineFieldType({
210
+ kind: "object",
211
+ container: true,
212
+ props: v.object({ fields: v.array(childField) }),
213
+ schema: (field, ctx) => ctx.buildFields(field.fields, [...ctx.path, "fields"])
214
+ }),
215
+ array: defineFieldType({
216
+ kind: "array",
217
+ container: true,
218
+ props: v.pipe(v.object({
219
+ of: v.optional(childField),
220
+ fields: v.optional(v.array(childField))
221
+ }), v.check((props) => props.of === void 0 !== (props.fields === void 0), "Give exactly one of \"of\" and \"fields\"")),
222
+ schema: (field, ctx) => v.array(field.of ? ctx.buildField(field.of, [...ctx.path, "of"]) : ctx.buildFields(field.fields ?? [], [...ctx.path, "fields"]))
223
+ })
224
+ };
150
225
  // src/field-types/date-types.ts
151
226
  const outputProps = v.object({
152
227
  /** `'date'` turns the ISO string into a `Date`. Defaults to `'string'`. */
@@ -221,7 +296,8 @@ const defaultFieldTypes = {
221
296
  ...dateTypes,
222
297
  ...choiceTypes,
223
298
  file: fileType,
224
- ...displayTypes
299
+ ...displayTypes,
300
+ ...containerTypes
225
301
  };
226
302
  // src/kinds/default-kinds.ts
227
303
  const isNil = (input) => input === void 0 || input === null;
@@ -272,7 +348,7 @@ const any = [
272
348
  "string",
273
349
  "boolean"
274
350
  ];
275
- const meta$6 = (label) => ({
351
+ const meta$7 = (label) => ({
276
352
  label,
277
353
  category: "comparison"
278
354
  });
@@ -283,54 +359,54 @@ const comparisonRules = {
283
359
  minValue: defineRule({
284
360
  kinds: ordered,
285
361
  params: valueParams,
286
- meta: meta$6("Minimum"),
362
+ meta: meta$7("Minimum"),
287
363
  create: (p, m) => v.minValue(p.value, m)
288
364
  }),
289
365
  maxValue: defineRule({
290
366
  kinds: ordered,
291
367
  params: valueParams,
292
- meta: meta$6("Maximum"),
368
+ meta: meta$7("Maximum"),
293
369
  create: (p, m) => v.maxValue(p.value, m)
294
370
  }),
295
371
  gtValue: defineRule({
296
372
  kinds: ordered,
297
373
  params: valueParams,
298
- meta: meta$6("Greater than"),
374
+ meta: meta$7("Greater than"),
299
375
  create: (p, m) => v.gtValue(p.value, m)
300
376
  }),
301
377
  ltValue: defineRule({
302
378
  kinds: ordered,
303
379
  params: valueParams,
304
- meta: meta$6("Less than"),
380
+ meta: meta$7("Less than"),
305
381
  create: (p, m) => v.ltValue(p.value, m)
306
382
  }),
307
383
  value: defineRule({
308
384
  kinds: any,
309
385
  params: valueParams,
310
- meta: meta$6("Equals"),
386
+ meta: meta$7("Equals"),
311
387
  create: (p, m) => v.value(p.value, m)
312
388
  }),
313
389
  notValue: defineRule({
314
390
  kinds: any,
315
391
  params: valueParams,
316
- meta: meta$6("Does not equal"),
392
+ meta: meta$7("Does not equal"),
317
393
  create: (p, m) => v.notValue(p.value, m)
318
394
  }),
319
395
  values: defineRule({
320
396
  kinds: ordered,
321
397
  params: valuesParams,
322
- meta: meta$6("One of"),
398
+ meta: meta$7("One of"),
323
399
  create: (p, m) => v.values(p.values, m)
324
400
  }),
325
401
  notValues: defineRule({
326
402
  kinds: ordered,
327
403
  params: valuesParams,
328
- meta: meta$6("None of"),
404
+ meta: meta$7("None of"),
329
405
  create: (p, m) => v.notValues(p.values, m)
330
406
  })
331
407
  };
332
408
  // src/rules/content-rules.ts
333
- const meta$5 = (label, description) => ({
409
+ const meta$6 = (label, description) => ({
334
410
  label,
335
411
  description,
336
412
  category: "content"
@@ -359,31 +435,31 @@ const contentRules = {
359
435
  regex: defineRule({
360
436
  kinds: ["string"],
361
437
  params: regexParams,
362
- meta: meta$5("Matches pattern", "A JavaScript regular expression, without the slashes."),
438
+ meta: meta$6("Matches pattern", "A JavaScript regular expression, without the slashes."),
363
439
  create: (p, m) => v.regex(new RegExp(p.pattern, p.flags?.replace(/[gy]/g, "")), m)
364
440
  }),
365
441
  startsWith: defineRule({
366
442
  kinds: ["string"],
367
443
  params: stringParams,
368
- meta: meta$5("Starts with"),
444
+ meta: meta$6("Starts with"),
369
445
  create: (p, m) => v.startsWith(p.value, m)
370
446
  }),
371
447
  endsWith: defineRule({
372
448
  kinds: ["string"],
373
449
  params: stringParams,
374
- meta: meta$5("Ends with"),
450
+ meta: meta$6("Ends with"),
375
451
  create: (p, m) => v.endsWith(p.value, m)
376
452
  }),
377
453
  includes: defineRule({
378
454
  kinds: ["string", "array"],
379
455
  params: contentParams,
380
- meta: meta$5("Contains"),
456
+ meta: meta$6("Contains"),
381
457
  create: (p, m) => v.includes(p.value, m)
382
458
  }),
383
459
  excludes: defineRule({
384
460
  kinds: ["string", "array"],
385
461
  params: contentParams,
386
- meta: meta$5("Does not contain"),
462
+ meta: meta$6("Does not contain"),
387
463
  create: (p, m) => v.excludes(p.value, m)
388
464
  })
389
465
  };
@@ -407,7 +483,7 @@ const dateRules = {
407
483
  };
408
484
  // src/rules/file-rules.ts
409
485
  const kinds$3 = ["file"];
410
- const meta$4 = (label, description) => ({
486
+ const meta$5 = (label, description) => ({
411
487
  label,
412
488
  description,
413
489
  category: "file"
@@ -419,31 +495,31 @@ const fileRules = {
419
495
  minSize: defineRule({
420
496
  kinds: kinds$3,
421
497
  params: countParams,
422
- meta: meta$4("Minimum size", bytes),
498
+ meta: meta$5("Minimum size", bytes),
423
499
  create: (p, m) => v.minSize(p.value, m)
424
500
  }),
425
501
  maxSize: defineRule({
426
502
  kinds: kinds$3,
427
503
  params: countParams,
428
- meta: meta$4("Maximum size", bytes),
504
+ meta: meta$5("Maximum size", bytes),
429
505
  create: (p, m) => v.maxSize(p.value, m)
430
506
  }),
431
507
  size: defineRule({
432
508
  kinds: kinds$3,
433
509
  params: countParams,
434
- meta: meta$4("Exact size", bytes),
510
+ meta: meta$5("Exact size", bytes),
435
511
  create: (p, m) => v.size(p.value, m)
436
512
  }),
437
513
  notSize: defineRule({
438
514
  kinds: kinds$3,
439
515
  params: countParams,
440
- meta: meta$4("Not this size", bytes),
516
+ meta: meta$5("Not this size", bytes),
441
517
  create: (p, m) => v.notSize(p.value, m)
442
518
  }),
443
519
  mimeType: defineRule({
444
520
  kinds: kinds$3,
445
521
  params: mimeTypeParams,
446
- meta: meta$4("File type"),
522
+ meta: meta$5("File type"),
447
523
  create: (p, m) => v.mimeType(p.types, m)
448
524
  })
449
525
  };
@@ -514,7 +590,7 @@ const formatRules = {
514
590
  };
515
591
  // src/rules/length-rules.ts
516
592
  const kinds$2 = ["string", "array"];
517
- const meta$3 = (label) => ({
593
+ const meta$4 = (label) => ({
518
594
  label,
519
595
  category: "length"
520
596
  });
@@ -523,31 +599,31 @@ const lengthRules = {
523
599
  length: defineRule({
524
600
  kinds: kinds$2,
525
601
  params: countParams,
526
- meta: meta$3("Exact length"),
602
+ meta: meta$4("Exact length"),
527
603
  create: (p, m) => v.length(p.value, m)
528
604
  }),
529
605
  minLength: defineRule({
530
606
  kinds: kinds$2,
531
607
  params: countParams,
532
- meta: meta$3("Minimum length"),
608
+ meta: meta$4("Minimum length"),
533
609
  create: (p, m) => v.minLength(p.value, m)
534
610
  }),
535
611
  maxLength: defineRule({
536
612
  kinds: kinds$2,
537
613
  params: countParams,
538
- meta: meta$3("Maximum length"),
614
+ meta: meta$4("Maximum length"),
539
615
  create: (p, m) => v.maxLength(p.value, m)
540
616
  }),
541
617
  notLength: defineRule({
542
618
  kinds: kinds$2,
543
619
  params: countParams,
544
- meta: meta$3("Not this length"),
620
+ meta: meta$4("Not this length"),
545
621
  create: (p, m) => v.notLength(p.value, m)
546
622
  })
547
623
  };
548
624
  // src/rules/normalize-rules.ts
549
625
  const stage = "normalize";
550
- const meta$2 = (label, description) => ({
626
+ const meta$3 = (label, description) => ({
551
627
  label,
552
628
  description,
553
629
  category: "normalize"
@@ -561,31 +637,31 @@ const normalizeRules = {
561
637
  trim: defineRule({
562
638
  kinds: ["string"],
563
639
  stage,
564
- meta: meta$2("Trim"),
640
+ meta: meta$3("Trim"),
565
641
  create: () => v.trim()
566
642
  }),
567
643
  trimStart: defineRule({
568
644
  kinds: ["string"],
569
645
  stage,
570
- meta: meta$2("Trim start"),
646
+ meta: meta$3("Trim start"),
571
647
  create: () => v.trimStart()
572
648
  }),
573
649
  trimEnd: defineRule({
574
650
  kinds: ["string"],
575
651
  stage,
576
- meta: meta$2("Trim end"),
652
+ meta: meta$3("Trim end"),
577
653
  create: () => v.trimEnd()
578
654
  }),
579
655
  toLowerCase: defineRule({
580
656
  kinds: ["string"],
581
657
  stage,
582
- meta: meta$2("Lowercase"),
658
+ meta: meta$3("Lowercase"),
583
659
  create: () => v.toLowerCase()
584
660
  }),
585
661
  toUpperCase: defineRule({
586
662
  kinds: ["string"],
587
663
  stage,
588
- meta: meta$2("Uppercase"),
664
+ meta: meta$3("Uppercase"),
589
665
  create: () => v.toUpperCase()
590
666
  }),
591
667
  normalize: defineRule({
@@ -597,27 +673,27 @@ const normalizeRules = {
597
673
  "NFKC",
598
674
  "NFKD"
599
675
  ])) }),
600
- meta: meta$2("Unicode normalize", "Defaults to NFC."),
676
+ meta: meta$3("Unicode normalize", "Defaults to NFC."),
601
677
  create: (p) => v.normalize(p.form)
602
678
  }),
603
679
  toMinValue: defineRule({
604
680
  kinds: ["number"],
605
681
  stage,
606
682
  params: limit,
607
- meta: meta$2("Raise to minimum", "Replaces a smaller number with the minimum."),
683
+ meta: meta$3("Raise to minimum", "Replaces a smaller number with the minimum."),
608
684
  create: (p) => v.toMinValue(p.value)
609
685
  }),
610
686
  toMaxValue: defineRule({
611
687
  kinds: ["number"],
612
688
  stage,
613
689
  params: limit,
614
- meta: meta$2("Lower to maximum", "Replaces a larger number with the maximum."),
690
+ meta: meta$3("Lower to maximum", "Replaces a larger number with the maximum."),
615
691
  create: (p) => v.toMaxValue(p.value)
616
692
  })
617
693
  };
618
694
  // src/rules/number-rules.ts
619
695
  const kinds$1 = ["number"];
620
- const meta$1 = (label) => ({
696
+ const meta$2 = (label) => ({
621
697
  label,
622
698
  category: "number"
623
699
  });
@@ -626,29 +702,29 @@ const divisor = v.pipe(v.number(), v.finite(), v.notValue(0, "Can't be 0"));
626
702
  const numberRules = {
627
703
  integer: defineRule({
628
704
  kinds: kinds$1,
629
- meta: meta$1("Whole number"),
705
+ meta: meta$2("Whole number"),
630
706
  create: (_, m) => v.integer(m)
631
707
  }),
632
708
  safeInteger: defineRule({
633
709
  kinds: kinds$1,
634
- meta: meta$1("Safe whole number"),
710
+ meta: meta$2("Safe whole number"),
635
711
  create: (_, m) => v.safeInteger(m)
636
712
  }),
637
713
  finite: defineRule({
638
714
  kinds: kinds$1,
639
- meta: meta$1("Finite number"),
715
+ meta: meta$2("Finite number"),
640
716
  create: (_, m) => v.finite(m)
641
717
  }),
642
718
  multipleOf: defineRule({
643
719
  kinds: kinds$1,
644
720
  params: v.object({ value: divisor }),
645
- meta: meta$1("Multiple of"),
721
+ meta: meta$2("Multiple of"),
646
722
  create: (p, m) => v.multipleOf(p.value, m)
647
723
  })
648
724
  };
649
725
  // src/rules/text-unit-rules.ts
650
726
  const kinds = ["string"];
651
- const meta = (label, description) => ({
727
+ const meta$1 = (label, description) => ({
652
728
  label,
653
729
  description,
654
730
  category: "text"
@@ -670,73 +746,73 @@ const textUnitRules = {
670
746
  graphemes: defineRule({
671
747
  kinds,
672
748
  params: countParams,
673
- meta: meta("Exact characters", graphemesInfo),
749
+ meta: meta$1("Exact characters", graphemesInfo),
674
750
  create: (p, m) => v.graphemes(p.value, m)
675
751
  }),
676
752
  minGraphemes: defineRule({
677
753
  kinds,
678
754
  params: countParams,
679
- meta: meta("Minimum characters", graphemesInfo),
755
+ meta: meta$1("Minimum characters", graphemesInfo),
680
756
  create: (p, m) => v.minGraphemes(p.value, m)
681
757
  }),
682
758
  maxGraphemes: defineRule({
683
759
  kinds,
684
760
  params: countParams,
685
- meta: meta("Maximum characters", graphemesInfo),
761
+ meta: meta$1("Maximum characters", graphemesInfo),
686
762
  create: (p, m) => v.maxGraphemes(p.value, m)
687
763
  }),
688
764
  notGraphemes: defineRule({
689
765
  kinds,
690
766
  params: countParams,
691
- meta: meta("Not this many characters", graphemesInfo),
767
+ meta: meta$1("Not this many characters", graphemesInfo),
692
768
  create: (p, m) => v.notGraphemes(p.value, m)
693
769
  }),
694
770
  words: defineRule({
695
771
  kinds,
696
772
  params: wordsParams,
697
- meta: meta("Exact words"),
773
+ meta: meta$1("Exact words"),
698
774
  create: (p, m) => v.words(p.locale, p.value, m)
699
775
  }),
700
776
  minWords: defineRule({
701
777
  kinds,
702
778
  params: wordsParams,
703
- meta: meta("Minimum words"),
779
+ meta: meta$1("Minimum words"),
704
780
  create: (p, m) => v.minWords(p.locale, p.value, m)
705
781
  }),
706
782
  maxWords: defineRule({
707
783
  kinds,
708
784
  params: wordsParams,
709
- meta: meta("Maximum words"),
785
+ meta: meta$1("Maximum words"),
710
786
  create: (p, m) => v.maxWords(p.locale, p.value, m)
711
787
  }),
712
788
  notWords: defineRule({
713
789
  kinds,
714
790
  params: wordsParams,
715
- meta: meta("Not this many words"),
791
+ meta: meta$1("Not this many words"),
716
792
  create: (p, m) => v.notWords(p.locale, p.value, m)
717
793
  }),
718
794
  bytes: defineRule({
719
795
  kinds,
720
796
  params: countParams,
721
- meta: meta("Exact bytes", bytesInfo),
797
+ meta: meta$1("Exact bytes", bytesInfo),
722
798
  create: (p, m) => v.bytes(p.value, m)
723
799
  }),
724
800
  minBytes: defineRule({
725
801
  kinds,
726
802
  params: countParams,
727
- meta: meta("Minimum bytes", bytesInfo),
803
+ meta: meta$1("Minimum bytes", bytesInfo),
728
804
  create: (p, m) => v.minBytes(p.value, m)
729
805
  }),
730
806
  maxBytes: defineRule({
731
807
  kinds,
732
808
  params: countParams,
733
- meta: meta("Maximum bytes", bytesInfo),
809
+ meta: meta$1("Maximum bytes", bytesInfo),
734
810
  create: (p, m) => v.maxBytes(p.value, m)
735
811
  }),
736
812
  notBytes: defineRule({
737
813
  kinds,
738
814
  params: countParams,
739
- meta: meta("Not this many bytes", bytesInfo),
815
+ meta: meta$1("Not this many bytes", bytesInfo),
740
816
  create: (p, m) => v.notBytes(p.value, m)
741
817
  })
742
818
  };
@@ -753,10 +829,71 @@ const defaultRules = {
753
829
  ...fileRules,
754
830
  ...normalizeRules
755
831
  };
832
+ // src/rules/define-form-rule.ts
833
+ const defineFormRule = (def) => def;
834
+ // src/rules/form-rules.ts
835
+ const meta = (label, description) => ({
836
+ label,
837
+ description,
838
+ category: "comparison"
839
+ });
840
+ const comparable = (value) => value instanceof Date ? value.getTime() : value;
841
+ const otherParams = v.object({ other: v.string() });
842
+ const operators = {
843
+ lt: (a, b) => a < b,
844
+ lte: (a, b) => a <= b,
845
+ gt: (a, b) => a > b,
846
+ gte: (a, b) => a >= b
847
+ };
848
+ /** Every built-in form rule. */
849
+ const defaultFormRules = {
850
+ sameAs: defineFormRule({
851
+ params: otherParams,
852
+ reads: (params) => [params.other],
853
+ kinds: [
854
+ "string",
855
+ "number",
856
+ "boolean",
857
+ "date"
858
+ ],
859
+ sameKind: true,
860
+ meta: meta("Same as", "Equals another field, e.g. to confirm a password"),
861
+ check: ([value, other]) => comparable(value) === comparable(other)
862
+ }),
863
+ differentFrom: defineFormRule({
864
+ params: otherParams,
865
+ reads: (params) => [params.other],
866
+ kinds: [
867
+ "string",
868
+ "number",
869
+ "boolean",
870
+ "date"
871
+ ],
872
+ sameKind: true,
873
+ meta: meta("Different from", "Differs from another field, e.g. a new password"),
874
+ check: ([value, other]) => comparable(value) !== comparable(other)
875
+ }),
876
+ compare: defineFormRule({
877
+ params: v.object({
878
+ op: v.picklist([
879
+ "lt",
880
+ "lte",
881
+ "gt",
882
+ "gte"
883
+ ]),
884
+ other: v.string()
885
+ }),
886
+ reads: (params) => [params.other],
887
+ kinds: ["number", "date"],
888
+ sameKind: true,
889
+ meta: meta("Compare", "Less or greater than another field, e.g. an end date after the start"),
890
+ check: ([value, other], { op }) => operators[op](comparable(value), comparable(other))
891
+ })
892
+ };
756
893
  // src/builder/registry.ts
757
894
  /** Consumer entries replace defaults with the same key. */
758
895
  function mergeRegistry(custom) {
759
- const rules = custom.includeDefaultRules === false ? {} : defaultRules;
896
+ const defaults = custom.includeDefaultRules !== false;
760
897
  return {
761
898
  kinds: Object.freeze({
762
899
  ...defaultKinds,
@@ -767,31 +904,18 @@ function mergeRegistry(custom) {
767
904
  ...custom.fieldTypes
768
905
  }),
769
906
  rules: Object.freeze({
770
- ...rules,
907
+ ...defaults ? defaultRules : {},
771
908
  ...custom.rules
909
+ }),
910
+ formRules: Object.freeze({
911
+ ...defaults ? defaultFormRules : {},
912
+ ...custom.formRules
772
913
  })
773
914
  };
774
915
  }
775
916
  function lookup(record, key) {
776
917
  return Object.hasOwn(record, key) ? record[key] : void 0;
777
918
  }
778
- // src/errors/definition-error.ts
779
- function formatPath(path) {
780
- return path.reduce((out, key) => {
781
- if (typeof key === "number") return `${out}[${key}]`;
782
- return out ? `${out}.${key}` : key;
783
- }, "");
784
- }
785
- /** Thrown by `build()` when the definition has issues. Lists all of them. */
786
- var DefinitionError = class extends Error {
787
- issues;
788
- constructor(issues) {
789
- const lines = issues.map((issue) => ` ${formatPath(issue.path) || "(root)"}: ${issue.message}`);
790
- super(`Invalid form definition:\n${lines.join("\n")}`);
791
- this.name = "DefinitionError";
792
- this.issues = issues;
793
- }
794
- };
795
919
  // src/rules/rule-params.ts
796
920
  const cache = /* @__PURE__ */ new WeakMap();
797
921
  function paramsFor(def, kind) {
@@ -808,34 +932,7 @@ function paramsFor(def, kind) {
808
932
  }
809
933
  return params;
810
934
  }
811
- // src/shared/reserved-keys.ts
812
- /**
813
- * Field keys this package owns. A field type's props can't use them.
814
- */
815
- const RESERVED_FIELD_KEYS = [
816
- "name",
817
- "type",
818
- "label",
819
- "description",
820
- "placeholder",
821
- "default",
822
- "required",
823
- "rules",
824
- "ui"
825
- ];
826
- /**
827
- * Rule keys this package owns. A rule's params can't use them.
828
- */
829
- const RESERVED_RULE_KEYS = ["rule", "message"];
830
- const fieldKeys = new Set(RESERVED_FIELD_KEYS);
831
- const ruleKeys = new Set(RESERVED_RULE_KEYS);
832
- const isReservedFieldKey = (key) => fieldKeys.has(key);
833
- const isReservedRuleKey = (key) => ruleKeys.has(key);
834
935
  // src/builder/validate-definition.ts
835
- const formShape = v.object({
836
- version: v.string(),
837
- fields: v.array(v.unknown())
838
- });
839
936
  const fieldShape = v.looseObject({
840
937
  name: v.optional(v.string()),
841
938
  type: v.string(),
@@ -847,40 +944,18 @@ const fieldShape = v.looseObject({
847
944
  rules: v.optional(v.array(v.unknown())),
848
945
  ui: v.optional(v.record(v.string(), v.unknown()))
849
946
  });
850
- const ruleShape = v.looseObject({
947
+ const ruleShape$1 = v.looseObject({
851
948
  rule: v.string(),
852
949
  message: v.optional(v.string())
853
950
  });
854
951
  const noParams = v.object({});
855
952
  /**
856
- * Checks a whole definition and collects every issue, in document order. Never throws.
953
+ * Checks one field's shape, name, type, props and rules, and adds its issues to `issues`.
954
+ * Returns the field ready to build, or undefined when it has issues or no value.
955
+ *
956
+ * `names` holds the names used so far at this level. It is undefined for the field of a list's
957
+ * items, which needs no name.
857
958
  */
858
- function checkDefinition(json, registry) {
859
- const issues = [];
860
- const fields = [];
861
- const form = v.safeParse(formShape, json);
862
- if (!form.success) return {
863
- issues: toIssues("invalid_shape", [], form.issues),
864
- fields
865
- };
866
- if (form.output.version !== "1") return {
867
- issues: [{
868
- code: "unsupported_version",
869
- path: ["version"],
870
- message: `Unsupported version "${form.output.version}". This package reads version '1'.`
871
- }],
872
- fields
873
- };
874
- const names = /* @__PURE__ */ new Set();
875
- form.output.fields.forEach((raw, i) => {
876
- const field = checkField(raw, ["fields", i], registry, names, issues);
877
- if (field) fields.push(field);
878
- });
879
- return {
880
- issues,
881
- fields
882
- };
883
- }
884
959
  function checkField(raw, path, registry, names, issues) {
885
960
  const shape = v.safeParse(fieldShape, raw);
886
961
  if (!shape.success) {
@@ -891,9 +966,13 @@ function checkField(raw, path, registry, names, issues) {
891
966
  const issueCount = issues.length;
892
967
  const type = lookup(registry.fieldTypes, field.type);
893
968
  const hasValue = type?.kind !== "none";
894
- if (!field.name) {
969
+ if (!names) {} else if (!field.name) {
895
970
  if (type && hasValue) issues.push(issue("missing_name", [...path, "name"], "A field needs a name"));
896
971
  } else if (hasValue) {
972
+ if (/[.[\]]/.test(field.name)) {
973
+ const message = `Name "${field.name}" can't contain ".", "[" or "]"`;
974
+ issues.push(issue("invalid_name", [...path, "name"], message));
975
+ }
897
976
  if (names.has(field.name)) issues.push(issue("duplicate_name", [...path, "name"], `Duplicate name "${field.name}"`));
898
977
  names.add(field.name);
899
978
  }
@@ -907,6 +986,10 @@ function checkField(raw, path, registry, names, issues) {
907
986
  const message = `A "${field.type}" field has no value, so it can't have "${key}"`;
908
987
  issues.push(issue("invalid_shape", [...path, key], message));
909
988
  }
989
+ if (!names) {
990
+ const message = `A list's items need a value, and a "${field.type}" field has none`;
991
+ issues.push(issue("invalid_shape", [...path, "type"], message));
992
+ }
910
993
  return;
911
994
  }
912
995
  const withProps = props && {
@@ -919,12 +1002,18 @@ function checkField(raw, path, registry, names, issues) {
919
1002
  kind,
920
1003
  itemKind: kind === "array" ? resolveKind(type.itemKind, withProps) : void 0
921
1004
  };
1005
+ if (type.container && kind === "object") {
1006
+ for (const key of ["required", "default"]) if (field[key] !== void 0) {
1007
+ const message = `A "${field.type}" field is always there, so it can't have "${key}"`;
1008
+ issues.push(issue("invalid_shape", [...path, key], message));
1009
+ }
1010
+ }
922
1011
  const fieldRules = (shape.output.rules ?? []).flatMap((rule, j) => checkRule(rule, kinds, [
923
1012
  ...path,
924
1013
  "rules",
925
1014
  j
926
1015
  ], registry, issues) ?? []);
927
- if (issues.length > issueCount || !field.name || !withProps) return void 0;
1016
+ if (issues.length > issueCount || names && !field.name || !withProps) return void 0;
928
1017
  const overridden = new Set(fieldRules.map((rule) => rule.name));
929
1018
  const defaultRules = checkDefaultRules(field.type, type, kinds, registry).filter((rule) => !overridden.has(rule.name));
930
1019
  return {
@@ -933,6 +1022,7 @@ function checkField(raw, path, registry, names, issues) {
933
1022
  path,
934
1023
  type,
935
1024
  kind: kindDef(field.type, kind, registry),
1025
+ valueKind: kind,
936
1026
  rules: [...defaultRules, ...fieldRules]
937
1027
  };
938
1028
  }
@@ -970,7 +1060,7 @@ function checkProps(field, type, path, issues) {
970
1060
  issues.push(...toIssues("invalid_props", path, parsed.issues));
971
1061
  }
972
1062
  function checkRule(raw, { kind, itemKind }, path, registry, issues) {
973
- const shape = v.safeParse(ruleShape, raw);
1063
+ const shape = v.safeParse(ruleShape$1, raw);
974
1064
  if (!shape.success) {
975
1065
  issues.push(...toIssues("invalid_shape", path, shape.issues));
976
1066
  return;
@@ -988,31 +1078,42 @@ function checkRule(raw, { kind, itemKind }, path, registry, issues) {
988
1078
  issues.push(issue("kind_mismatch", [...path, "rule"], message));
989
1079
  return;
990
1080
  }
1081
+ const params = checkParams(rule, paramsFor(def, each ? itemKind : kind), isReservedRuleKey, `Rule "${rule.rule}"`, path, issues);
1082
+ if (!params) return void 0;
1083
+ return {
1084
+ name: rule.rule,
1085
+ def,
1086
+ params,
1087
+ message: rule.message,
1088
+ each
1089
+ };
1090
+ }
1091
+ /**
1092
+ * Checks the params of a rule or a form rule. Every key that isn't reserved must be one of
1093
+ * `params`, and together they must pass it. Returns the parsed params, or undefined when they
1094
+ * have issues. `at` names the rule in messages, e.g. `Rule "minLength"`.
1095
+ */
1096
+ function checkParams(raw, params, isReserved, at, path, issues) {
991
1097
  const issueCount = issues.length;
992
- const params = paramsFor(def, each ? itemKind : kind);
993
1098
  const paramKeys = Object.keys(params?.entries ?? {});
994
1099
  const input = {};
995
- for (const key of Object.keys(rule)) {
996
- if (isReservedRuleKey(key)) continue;
997
- if (paramKeys.includes(key)) input[key] = rule[key];
998
- else issues.push(issue("unknown_param", [...path, key], `Rule "${rule.rule}" has no param "${key}"`));
1100
+ for (const key of Object.keys(raw)) {
1101
+ if (isReserved(key)) continue;
1102
+ if (paramKeys.includes(key)) input[key] = raw[key];
1103
+ else issues.push(issue("unknown_param", [...path, key], `${at} has no param "${key}"`));
999
1104
  }
1000
1105
  const parsed = v.safeParse(params ?? noParams, input);
1001
1106
  if (!parsed.success) issues.push(...toIssues("invalid_params", path, parsed.issues));
1002
1107
  if (!parsed.success || issues.length > issueCount) return void 0;
1003
- return {
1004
- name: rule.rule,
1005
- def,
1006
- params: parsed.output,
1007
- message: rule.message,
1008
- each
1009
- };
1108
+ return parsed.output;
1010
1109
  }
1011
1110
  function checkRegistry(registry) {
1012
1111
  for (const [name, rule] of Object.entries(registry.rules)) for (const kind of rule.kinds) for (const key of Object.keys(paramsFor(rule, kind)?.entries ?? {})) if (isReservedRuleKey(key)) throw new TypeError(`Rule "${name}": params can't use the reserved key "${key}"`);
1112
+ for (const [name, rule] of Object.entries(registry.formRules)) for (const key of Object.keys(rule.params?.entries ?? {})) if (isReservedFormRuleKey(key)) throw new TypeError(`Form rule "${name}": params can't use the reserved key "${key}"`);
1013
1113
  for (const [name, type] of Object.entries(registry.fieldTypes)) {
1014
1114
  const at = `Field type "${name}"`;
1015
1115
  for (const kind of [type.kind, type.itemKind]) if (typeof kind === "string" && !lookup(registry.kinds, kind)) throw new TypeError(`${at}: kind "${kind}" is not registered`);
1116
+ if (type.container && type.kind !== "object" && type.kind !== "array") throw new TypeError(`${at}: a container's kind must be 'object' or 'array'`);
1016
1117
  if (type.kind !== "none" && !type.schema) throw new TypeError(`${at}: schema() is required unless the kind is 'none'`);
1017
1118
  for (const key of Object.keys(type.props?.entries ?? {})) if (isReservedFieldKey(key)) throw new TypeError(`${at}: props can't use the reserved key "${key}"`);
1018
1119
  if (typeof type.kind === "string" && typeof type.itemKind !== "function") checkDefaultRules(name, type, {
@@ -1034,6 +1135,293 @@ function toIssues(code, path, valibotIssues) {
1034
1135
  return issue(code, [...path, ...keys], valibotIssue.message);
1035
1136
  });
1036
1137
  }
1138
+ // src/builder/check-form-rules.ts
1139
+ const ruleShape = v.looseObject({
1140
+ rule: v.string(),
1141
+ field: v.string(),
1142
+ message: v.string()
1143
+ });
1144
+ /**
1145
+ * Checks the definition's form rules against the fields the compiler built, and adds their
1146
+ * issues to `issues`. Returns the rules that are ready to build.
1147
+ */
1148
+ function checkFormRules(rules, registry, fieldsByPath, issues) {
1149
+ return rules.flatMap((raw, i) => checkFormRule(raw, ["formRules", i], registry, fieldsByPath, issues) ?? []);
1150
+ }
1151
+ function checkFormRule(raw, path, registry, fieldsByPath, issues) {
1152
+ const shape = v.safeParse(ruleShape, raw);
1153
+ if (!shape.success) {
1154
+ issues.push(...toIssues("invalid_shape", path, shape.issues));
1155
+ return;
1156
+ }
1157
+ const rule = shape.output;
1158
+ const at = `Form rule "${rule.rule}"`;
1159
+ const def = lookup(registry.formRules, rule.rule);
1160
+ if (!def) {
1161
+ issues.push(issue("unknown_rule", [...path, "rule"], `Unknown form rule "${rule.rule}"`));
1162
+ return;
1163
+ }
1164
+ const params = checkParams(rule, def.params, isReservedFormRuleKey, at, path, issues);
1165
+ if (!params) return void 0;
1166
+ const dotPaths = [rule.field, ...def.reads(params)];
1167
+ const paths = dotPaths.map((dotPath) => dotPath.split("."));
1168
+ const fieldKeys = paths[0];
1169
+ const issueCount = issues.length;
1170
+ const fields = dotPaths.map((dotPath, i) => {
1171
+ const found = findField(dotPath, fieldsByPath);
1172
+ const message = typeof found === "string" ? found : i > 0 && outsideItem(paths[i], fieldKeys);
1173
+ if (!message) return found;
1174
+ issues.push(issue("unknown_field", i === 0 ? [...path, "field"] : path, message));
1175
+ return null;
1176
+ });
1177
+ if (issues.length > issueCount) return void 0;
1178
+ if (fields.some((field) => !field)) return void 0;
1179
+ const checked = fields;
1180
+ const kinds = checked.map((field) => field.valueKind);
1181
+ const wrong = kinds.find((kind) => def.kinds && !def.kinds.includes(kind));
1182
+ if (wrong !== void 0) {
1183
+ const message = `${at} can't be used on ${wrong} fields`;
1184
+ issues.push(issue("kind_mismatch", [...path, "rule"], message));
1185
+ return;
1186
+ }
1187
+ if (def.sameKind && new Set(kinds).size > 1) {
1188
+ const message = `${at} needs fields of one kind, not ${[...new Set(kinds)].join(" and ")}`;
1189
+ issues.push(issue("kind_mismatch", [...path, "rule"], message));
1190
+ return;
1191
+ }
1192
+ const field = checked[0].field;
1193
+ return {
1194
+ name: rule.rule,
1195
+ def,
1196
+ params,
1197
+ message: rule.message,
1198
+ path,
1199
+ field,
1200
+ paths
1201
+ };
1202
+ }
1203
+ /**
1204
+ * The field with a value at `dotPath`. Null when a field on the way has issues of its own, and
1205
+ * the issue's message when there is no such field.
1206
+ */
1207
+ function findField(dotPath, fieldsByPath) {
1208
+ const keys = dotPath.split(".");
1209
+ let found;
1210
+ for (let i = 1; i <= keys.length; i++) {
1211
+ found = fieldsByPath.get(keys.slice(0, i).join("."));
1212
+ if (found === null) return null;
1213
+ if (found) continue;
1214
+ if (keys[i - 1] === "*" && i < keys.length) continue;
1215
+ if (fieldsByPath.get(keys.slice(0, i - 1).join("."))?.valueKind === "array" && keys[i - 1] !== "*") return `Unknown field "${dotPath}". A list's items are read with "*": "${[
1216
+ ...keys.slice(0, i - 1),
1217
+ "*",
1218
+ ...keys.slice(i - 1)
1219
+ ].join(".")}"`;
1220
+ return `Unknown field "${dotPath}"`;
1221
+ }
1222
+ return found ?? `Unknown field "${dotPath}"`;
1223
+ }
1224
+ /**
1225
+ * A path into the items of a list is read once per item, so `field` must be in the same items.
1226
+ * Returns the issue's message when it isn't.
1227
+ */
1228
+ function outsideItem(keys, fieldKeys) {
1229
+ const star = keys.lastIndexOf("*");
1230
+ const item = (path) => path.slice(0, star + 1).join(".");
1231
+ if (star === -1 || item(keys) === item(fieldKeys)) return void 0;
1232
+ const list = keys.slice(0, star).join(".");
1233
+ return `"${keys.join(".")}" is read from each item of "${list}", so "${fieldKeys.join(".")}" must be too`;
1234
+ }
1235
+ // src/builder/form-rule-action.ts
1236
+ /**
1237
+ * Runs a form rule on the form's value and puts its issue on `field`. A rule on the fields of a
1238
+ * list's items runs once per item, with the item's index in place of each `'*'`.
1239
+ *
1240
+ * The rule only runs when none of the fields it reads has an issue. `v.partialCheck` can't do
1241
+ * this: it runs whenever the form has no schema issues, even when a field it reads failed a rule.
1242
+ */
1243
+ function formRuleAction(rule, message) {
1244
+ const { def, params, paths } = rule;
1245
+ const fieldKeys = paths[0];
1246
+ return v.rawCheck(({ dataset, addIssue }) => {
1247
+ for (const indexes of itemIndexes(dataset.value, fieldKeys)) {
1248
+ const read = paths.map((keys) => withIndexes(keys, indexes));
1249
+ if (read.some((path) => hasIssue(dataset.issues, path))) continue;
1250
+ const values = read.map((path) => valueAt(dataset.value, path));
1251
+ if (values.includes(void 0) || def.check(values, params)) continue;
1252
+ const field = read[0];
1253
+ addIssue({
1254
+ message,
1255
+ input: values[0],
1256
+ path: issuePath(dataset.value, field)
1257
+ });
1258
+ }
1259
+ });
1260
+ }
1261
+ /** Replaces each `'*'` in `keys` with the next index. */
1262
+ const withIndexes = (keys, indexes) => {
1263
+ let next = 0;
1264
+ return keys.map((key) => key === "*" ? indexes[next++] : key);
1265
+ };
1266
+ const valueAt = (input, path) => path.reduce((value, key) => typeof value === "object" && value !== null ? value[key] : void 0, input);
1267
+ /** True when an issue is on `path`, inside it, or on a value that holds it. */
1268
+ const hasIssue = (issues, path) => !!issues?.some((issue) => (issue.path ?? []).every((item, i) => i >= path.length || item.key === path[i]));
1269
+ /** Valibot's path to `path` in `input`, the way an object or array schema reports it. */
1270
+ const issuePath = (input, path) => path.map((key, i) => {
1271
+ const parent = valueAt(input, path.slice(0, i));
1272
+ return {
1273
+ type: Array.isArray(parent) ? "array" : "object",
1274
+ origin: "value",
1275
+ input: parent,
1276
+ key,
1277
+ value: valueAt(parent, [key])
1278
+ };
1279
+ });
1280
+ /** The indexes for each `'*'` in `keys`, for every item `value` has there. */
1281
+ function* itemIndexes(value, keys, indexes = []) {
1282
+ const star = keys.indexOf("*");
1283
+ if (star === -1) {
1284
+ yield indexes;
1285
+ return;
1286
+ }
1287
+ const list = valueAt(value, keys.slice(0, star));
1288
+ if (!Array.isArray(list)) return;
1289
+ for (const [i, item] of list.entries()) yield* itemIndexes(item, keys.slice(star + 1), [...indexes, i]);
1290
+ }
1291
+ // src/builder/compile-definition.ts
1292
+ const formShape = v.object({
1293
+ version: v.string(),
1294
+ fields: v.array(v.unknown()),
1295
+ formRules: v.optional(v.array(v.unknown()))
1296
+ });
1297
+ /**
1298
+ * Checks a definition and builds its schema in the same pass, so `validate()` and `build()`
1299
+ * always agree. Never throws on bad input.
1300
+ */
1301
+ function compileDefinition(json, registry, messages) {
1302
+ const issues = [];
1303
+ const form = v.safeParse(formShape, json);
1304
+ if (!form.success) return {
1305
+ issues: toIssues("invalid_shape", [], form.issues),
1306
+ schema: v.object({})
1307
+ };
1308
+ if (form.output.version !== "1") {
1309
+ const message = `Unsupported version "${form.output.version}". This package reads version '1'.`;
1310
+ issues.push({
1311
+ code: "unsupported_version",
1312
+ path: ["version"],
1313
+ message
1314
+ });
1315
+ return {
1316
+ issues,
1317
+ schema: v.object({})
1318
+ };
1319
+ }
1320
+ const compiler = createCompiler(registry, messages, issues);
1321
+ const schema = compiler.fields(form.output.fields, ["fields"]);
1322
+ const rules = checkFormRules(form.output.formRules ?? [], registry, compiler.fieldsByPath, issues);
1323
+ if (issues.length || !rules.length) return {
1324
+ issues,
1325
+ schema
1326
+ };
1327
+ return {
1328
+ issues,
1329
+ schema: rules.reduce((acc, rule) => {
1330
+ const ctx = {
1331
+ field: rule.field,
1332
+ rule: rule.name,
1333
+ path: rule.path
1334
+ };
1335
+ return v.pipe(acc, formRuleAction(rule, messages.resolve(rule.message, ctx)));
1336
+ }, schema)
1337
+ };
1338
+ }
1339
+ /** Every field the compiler checks adds its issues to `issues`. */
1340
+ function createCompiler(registry, messages, issues) {
1341
+ const context = (path) => ({
1342
+ path,
1343
+ buildFields: fields,
1344
+ buildField: field
1345
+ });
1346
+ const fieldsByPath = /* @__PURE__ */ new Map();
1347
+ const parents = [];
1348
+ const parentPath = () => parents.at(-1) ?? [];
1349
+ function fields(list, path) {
1350
+ const entries = {};
1351
+ const names = /* @__PURE__ */ new Set();
1352
+ list.forEach((raw, i) => {
1353
+ const checked = checkField(raw, [...path, i], registry, names, issues);
1354
+ const name = checked ? checked.name : nameOf(raw);
1355
+ if (name === void 0) return;
1356
+ const dataPath = [...parentPath(), name];
1357
+ remember(raw, checked, dataPath);
1358
+ if (checked) entries[name] = build(checked, dataPath);
1359
+ });
1360
+ return v.object(entries);
1361
+ }
1362
+ function field(raw, path) {
1363
+ const checked = checkField(raw, path, registry, void 0, issues);
1364
+ const dataPath = parentPath();
1365
+ remember(raw, checked, dataPath);
1366
+ return checked ? build(checked, dataPath) : v.never();
1367
+ }
1368
+ function build(checked, dataPath) {
1369
+ parents.push(checked.valueKind === "array" ? [...dataPath, "*"] : dataPath);
1370
+ try {
1371
+ const schema = buildFieldSchema(checked, messages, context(checked.path));
1372
+ checkDefault(checked, schema, issues);
1373
+ return schema;
1374
+ } finally {
1375
+ parents.pop();
1376
+ }
1377
+ }
1378
+ /** The first field at a path wins, as with duplicate names. Display elements are left out. */
1379
+ function remember(raw, checked, dataPath) {
1380
+ const key = dataPath.join(".");
1381
+ if (fieldsByPath.has(key) || !checked && !hasValue(raw, registry)) return;
1382
+ fieldsByPath.set(key, checked ?? null);
1383
+ }
1384
+ return {
1385
+ fields,
1386
+ field,
1387
+ fieldsByPath
1388
+ };
1389
+ }
1390
+ const isRecord = (value) => typeof value === "object" && value !== null;
1391
+ const nameOf = (raw) => isRecord(raw) && typeof raw.name === "string" ? raw.name : void 0;
1392
+ /** An unknown type counts as one with a value, so a form rule that reads the field is fine. */
1393
+ const hasValue = (raw, registry) => isRecord(raw) && typeof raw.type === "string" && lookup(registry.fieldTypes, raw.type)?.kind !== "none";
1394
+ /** A default must be a value the field accepts. An empty one is the same as none. */
1395
+ function checkDefault({ field, kind, path }, schema, issues) {
1396
+ if (field.default === void 0 || kind.isEmpty(field.default)) return;
1397
+ const result = v.safeParse(schema, field.default);
1398
+ if (!result.success) issues.push(...toIssues("invalid_default", [...path, "default"], result.issues));
1399
+ }
1400
+ // src/builder/build-context.ts
1401
+ const messages = {
1402
+ required: "Required",
1403
+ resolve: (text) => text
1404
+ };
1405
+ /**
1406
+ * A build context for testing a field type on its own, without a definition around it.
1407
+ * `buildFields` and `buildField` use the default registries and throw a `DefinitionError` when
1408
+ * the child fields have issues. `overrides` replaces any member, e.g. `path`.
1409
+ */
1410
+ function createBuildContext(overrides = {}) {
1411
+ const registry = mergeRegistry({});
1412
+ const run = (compile) => {
1413
+ const issues = [];
1414
+ const result = compile(createCompiler(registry, messages, issues));
1415
+ if (issues.length) throw new DefinitionError(issues);
1416
+ return result;
1417
+ };
1418
+ return {
1419
+ path: [],
1420
+ buildFields: (fields, path) => run((compiler) => compiler.fields(fields, path)),
1421
+ buildField: (field, path) => run((compiler) => compiler.field(field, path)),
1422
+ ...overrides
1423
+ };
1424
+ }
1037
1425
  // src/builder/create-schema-builder.ts
1038
1426
  function createSchemaBuilder(options = {}) {
1039
1427
  const registry = mergeRegistry(options);
@@ -1044,7 +1432,7 @@ function createSchemaBuilder(options = {}) {
1044
1432
  };
1045
1433
  /** Every issue in the definition, in document order. Never throws on bad input. */
1046
1434
  function validate(json) {
1047
- const { issues } = checkDefinition(json, registry);
1435
+ const { issues } = compileDefinition(json, registry, messages);
1048
1436
  return issues.length ? {
1049
1437
  success: false,
1050
1438
  issues
@@ -1054,11 +1442,9 @@ function createSchemaBuilder(options = {}) {
1054
1442
  };
1055
1443
  }
1056
1444
  function build(json) {
1057
- const { issues, fields } = checkDefinition(json, registry);
1445
+ const { issues, schema } = compileDefinition(json, registry, messages);
1058
1446
  if (issues.length) throw new DefinitionError(issues);
1059
- const entries = {};
1060
- for (const field of fields) entries[field.name] = buildFieldSchema(field, messages);
1061
- return v.object(entries);
1447
+ return schema;
1062
1448
  }
1063
1449
  /**
1064
1450
  * The registered rules that can be used on a field of `kind`, e.g. for a rule picker.
@@ -1072,9 +1458,66 @@ function createSchemaBuilder(options = {}) {
1072
1458
  rulesForKind,
1073
1459
  kinds: registry.kinds,
1074
1460
  fieldTypes: registry.fieldTypes,
1075
- rules: registry.rules
1461
+ rules: registry.rules,
1462
+ formRules: registry.formRules
1076
1463
  };
1077
1464
  }
1078
- export { DefinitionError, RESERVED_FIELD_KEYS, RESERVED_RULE_KEYS, comparisonRules, contentRules, createSchemaBuilder, dateRules, defaultFieldTypes, defaultKinds, defaultRules, defineFieldType, defineRule, fileRules, formatPath, formatRules, lengthRules, normalizeRules, numberRules, textUnitRules };
1465
+ // src/definition/initial-input.ts
1466
+ /**
1467
+ * Formisch's `initialInput` for a definition: each field's `default`, nested the way the value
1468
+ * is. A group gives an object, and an array without a default gives `[]`. Other fields without
1469
+ * a default are left out. Like `walkFields`, it reads the built-in `group` and `array` types
1470
+ * and needs no builder.
1471
+ */
1472
+ function getInitialInput(definition) {
1473
+ return inputOf(definition.fields);
1474
+ }
1475
+ function inputOf(fields) {
1476
+ const input = {};
1477
+ for (const field of fields) {
1478
+ if (field.name === void 0) continue;
1479
+ const value = initialValue(field);
1480
+ if (value !== void 0) input[field.name] = value;
1481
+ }
1482
+ return input;
1483
+ }
1484
+ function initialValue(field) {
1485
+ if (field.type === "group") return inputOf(Array.isArray(field.fields) ? field.fields : []);
1486
+ if (field.type === "array") return field.default ?? [];
1487
+ return field.default;
1488
+ }
1489
+ // src/definition/walk-fields.ts
1490
+ /**
1491
+ * Every field of a definition, at every depth, in document order. A group or an array comes
1492
+ * before its fields, and the fields of an array's items come once. It reads the built-in
1493
+ * `group` and `array` types and needs no builder, so it doesn't check the definition: run
1494
+ * `validate()` first.
1495
+ */
1496
+ function* walkFields(definition) {
1497
+ yield* walkList(definition.fields, ["fields"], [], 0);
1498
+ }
1499
+ function* walkList(fields, at, dataPath, depth) {
1500
+ for (const [i, field] of fields.entries()) {
1501
+ const path = field.name === void 0 ? dataPath : [...dataPath, field.name];
1502
+ yield* walkField(field, [...at, i], path, depth);
1503
+ }
1504
+ }
1505
+ function* walkField(field, definitionPath, dataPath, depth) {
1506
+ yield {
1507
+ field,
1508
+ type: field.type,
1509
+ dataPath,
1510
+ definitionPath,
1511
+ depth
1512
+ };
1513
+ if (field.type === "group" && Array.isArray(field.fields)) yield* walkList(field.fields, [...definitionPath, "fields"], dataPath, depth + 1);
1514
+ else if (field.type === "array") {
1515
+ const items = [...dataPath, "*"];
1516
+ if (isField(field.of)) yield* walkField(field.of, [...definitionPath, "of"], items, depth + 1);
1517
+ else if (Array.isArray(field.fields)) yield* walkList(field.fields, [...definitionPath, "fields"], items, depth + 1);
1518
+ }
1519
+ }
1520
+ const isField = (value) => typeof value === "object" && value !== null && "type" in value;
1521
+ export { DefinitionError, RESERVED_FIELD_KEYS, RESERVED_FORM_RULE_KEYS, RESERVED_RULE_KEYS, comparisonRules, contentRules, createBuildContext, createSchemaBuilder, dateRules, defaultFieldTypes, defaultFormRules, defaultKinds, defaultRules, defineFieldType, defineFormRule, defineRule, fileRules, formatPath, formatRules, getInitialInput, lengthRules, normalizeRules, numberRules, textUnitRules, walkFields };
1079
1522
 
1080
1523
  //# sourceMappingURL=index.js.map