@ic-reactor/candid 3.0.18-beta.1 → 3.1.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.
Files changed (59) hide show
  1. package/README.md +41 -14
  2. package/dist/display-reactor.d.ts.map +1 -1
  3. package/dist/display-reactor.js +1 -1
  4. package/dist/display-reactor.js.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/metadata-display-reactor.d.ts +4 -4
  10. package/dist/metadata-display-reactor.d.ts.map +1 -1
  11. package/dist/metadata-display-reactor.js +1 -1
  12. package/dist/metadata-display-reactor.js.map +1 -1
  13. package/dist/metadata-reactor.d.ts +51 -0
  14. package/dist/metadata-reactor.d.ts.map +1 -0
  15. package/dist/metadata-reactor.js +193 -0
  16. package/dist/metadata-reactor.js.map +1 -0
  17. package/dist/reactor.d.ts +3 -2
  18. package/dist/reactor.d.ts.map +1 -1
  19. package/dist/reactor.js +6 -0
  20. package/dist/reactor.js.map +1 -1
  21. package/dist/types.d.ts +10 -0
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/visitor/candid/helpers.d.ts +4 -0
  24. package/dist/visitor/candid/helpers.d.ts.map +1 -0
  25. package/dist/visitor/candid/helpers.js +84 -0
  26. package/dist/visitor/candid/helpers.js.map +1 -0
  27. package/dist/visitor/candid/index.d.ts +45 -0
  28. package/dist/visitor/candid/index.d.ts.map +1 -0
  29. package/dist/visitor/candid/index.js +417 -0
  30. package/dist/visitor/candid/index.js.map +1 -0
  31. package/dist/visitor/candid/types.d.ts +167 -0
  32. package/dist/visitor/candid/types.d.ts.map +1 -0
  33. package/dist/visitor/candid/types.js +2 -0
  34. package/dist/visitor/candid/types.js.map +1 -0
  35. package/dist/visitor/constants.d.ts +1 -1
  36. package/dist/visitor/constants.d.ts.map +1 -1
  37. package/dist/visitor/index.d.ts +1 -0
  38. package/dist/visitor/index.d.ts.map +1 -1
  39. package/dist/visitor/index.js +1 -0
  40. package/dist/visitor/index.js.map +1 -1
  41. package/dist/visitor/returns/index.d.ts.map +1 -1
  42. package/dist/visitor/returns/index.js +1 -1
  43. package/dist/visitor/returns/index.js.map +1 -1
  44. package/dist/visitor/returns/types.d.ts +1 -1
  45. package/dist/visitor/returns/types.d.ts.map +1 -1
  46. package/package.json +11 -7
  47. package/src/display-reactor.ts +1 -2
  48. package/src/index.ts +1 -0
  49. package/src/metadata-display-reactor.ts +4 -4
  50. package/src/metadata-reactor.ts +287 -0
  51. package/src/reactor.ts +12 -2
  52. package/src/types.ts +11 -0
  53. package/src/visitor/candid/helpers.ts +89 -0
  54. package/src/visitor/candid/index.ts +658 -0
  55. package/src/visitor/candid/types.ts +291 -0
  56. package/src/visitor/constants.ts +1 -1
  57. package/src/visitor/index.ts +1 -0
  58. package/src/visitor/returns/index.ts +6 -4
  59. package/src/visitor/returns/types.ts +0 -2
@@ -0,0 +1,658 @@
1
+ import { IDL } from "@icp-sdk/core/candid"
2
+ import { Principal } from "@icp-sdk/core/principal"
3
+ import type { BaseActor, FunctionName } from "@ic-reactor/core"
4
+ import * as z from "zod"
5
+ import { isQuery } from "../helpers"
6
+ import { formatLabel } from "../arguments/helpers"
7
+ import type {
8
+ FormServiceMeta,
9
+ FormArgumentsMeta,
10
+ FormFieldNode,
11
+ FormFieldType,
12
+ FormRenderHint,
13
+ VariableRefCandidate,
14
+ } from "./types"
15
+ import { cloneField, toFormValue } from "./helpers"
16
+
17
+ export * from "./types"
18
+
19
+ const COMPOUND_RENDER_HINT: FormRenderHint = {
20
+ isCompound: true,
21
+ isPrimitive: false,
22
+ }
23
+
24
+ const TEXT_RENDER_HINT: FormRenderHint = {
25
+ isCompound: false,
26
+ isPrimitive: true,
27
+ inputType: "text",
28
+ }
29
+
30
+ const NUMBER_RENDER_HINT: FormRenderHint = {
31
+ isCompound: false,
32
+ isPrimitive: true,
33
+ inputType: "number",
34
+ }
35
+
36
+ const CHECKBOX_RENDER_HINT: FormRenderHint = {
37
+ isCompound: false,
38
+ isPrimitive: true,
39
+ inputType: "checkbox",
40
+ }
41
+
42
+ const FILE_RENDER_HINT: FormRenderHint = {
43
+ isCompound: false,
44
+ isPrimitive: true,
45
+ inputType: "file",
46
+ }
47
+
48
+ /**
49
+ * Visitor that generates form-oriented metadata from Candid IDL types.
50
+ *
51
+ * Each generated field includes:
52
+ * - `schema` for validation
53
+ * - `component` for renderer selection
54
+ * - `renderHint` for UI behavior hints
55
+ */
56
+ export class CandidFormVisitor<A = BaseActor> extends IDL.Visitor<
57
+ string,
58
+ FormFieldNode | FormArgumentsMeta | FormServiceMeta<A>
59
+ > {
60
+ private recCache = new Map<IDL.RecClass<any>, FormFieldNode>()
61
+ private recursiveSchemas: Map<string, z.ZodTypeAny> = new Map()
62
+ private nameStack: string[] = []
63
+
64
+ private withName<T>(name: string, fn: () => T): T {
65
+ this.nameStack.push(name)
66
+ try {
67
+ return fn()
68
+ } finally {
69
+ this.nameStack.pop()
70
+ }
71
+ }
72
+
73
+ private currentName(): string {
74
+ return this.nameStack.join("")
75
+ }
76
+
77
+ public visitService(t: IDL.ServiceClass): FormServiceMeta<A> {
78
+ const result = {} as FormServiceMeta<A>
79
+ for (const [functionName, func] of t._fields) {
80
+ result[functionName as FunctionName<A>] = func.accept(
81
+ this,
82
+ functionName
83
+ ) as FormArgumentsMeta
84
+ }
85
+ return result
86
+ }
87
+
88
+ public visitFunc(t: IDL.FuncClass, functionName: string): FormArgumentsMeta {
89
+ const functionType = isQuery(t) ? "query" : "update"
90
+ const args = t.argTypes.map(
91
+ (argType, index) =>
92
+ this.withName(`[${index}]`, () =>
93
+ argType.accept(this, `__arg${index}`)
94
+ ) as FormFieldNode
95
+ )
96
+ const argCount = args.length
97
+ const schema =
98
+ argCount === 0
99
+ ? (z.tuple([]) as unknown as z.ZodTuple<
100
+ [z.ZodTypeAny, ...z.ZodTypeAny[]]
101
+ >)
102
+ : z.tuple(
103
+ args.map((field) => field.schema) as [
104
+ z.ZodTypeAny,
105
+ ...z.ZodTypeAny[],
106
+ ]
107
+ )
108
+
109
+ return {
110
+ candidType: t.name,
111
+ functionType,
112
+ functionName,
113
+ args,
114
+ defaults: args.map((arg) => arg.defaultValue),
115
+ argCount,
116
+ isEmpty: argCount === 0,
117
+ schema,
118
+ }
119
+ }
120
+
121
+ public buildFunctionMeta(
122
+ func: IDL.FuncClass,
123
+ functionName: string
124
+ ): FormArgumentsMeta {
125
+ return func.accept(this, functionName) as FormArgumentsMeta
126
+ }
127
+
128
+ public buildValueMeta(
129
+ valueType: IDL.Type,
130
+ functionName = "__value"
131
+ ): FormArgumentsMeta {
132
+ const valueField = this.withName("[0]", () =>
133
+ valueType.accept(this, "__arg0")
134
+ ) as FormFieldNode
135
+
136
+ return {
137
+ candidType: valueType.display?.() ?? valueType.name ?? "value",
138
+ functionType: "value",
139
+ functionName,
140
+ args: [valueField],
141
+ defaults: [valueField.defaultValue],
142
+ argCount: 1,
143
+ isEmpty: false,
144
+ schema: z.tuple([valueField.schema]),
145
+ }
146
+ }
147
+
148
+ public toFormValuesFromDecodedArgs(
149
+ fields: FormFieldNode[],
150
+ decodedArgs: unknown[]
151
+ ): unknown[] {
152
+ return fields.map((field, index) => toFormValue(field, decodedArgs[index]))
153
+ }
154
+
155
+ public collectRefCandidatesFromRoot(
156
+ sourceNodeId: string,
157
+ rootExpr: string,
158
+ rootLabel: string,
159
+ rootField: FormFieldNode
160
+ ): VariableRefCandidate[] {
161
+ const out: VariableRefCandidate[] = []
162
+ const walk = (field: FormFieldNode, expr: string, label: string) => {
163
+ out.push({
164
+ expr,
165
+ label,
166
+ candidType: field.candidType,
167
+ fieldType: field.type,
168
+ sourceNodeId,
169
+ })
170
+
171
+ switch (field.type) {
172
+ case "record":
173
+ case "tuple":
174
+ for (const child of field.fields) {
175
+ walk(child, `${expr}.${child.label}`, `${label}.${child.label}`)
176
+ }
177
+ break
178
+ case "variant":
179
+ for (const child of field.options) {
180
+ walk(child, `${expr}.${child.label}`, `${label}.${child.label}`)
181
+ }
182
+ break
183
+ case "optional":
184
+ walk(field.innerField, `${expr}.some`, `${label}.some`)
185
+ break
186
+ case "vector":
187
+ case "recursive":
188
+ case "unknown":
189
+ case "blob":
190
+ case "principal":
191
+ case "text":
192
+ case "number":
193
+ case "boolean":
194
+ case "null":
195
+ break
196
+ }
197
+ }
198
+
199
+ walk(rootField, rootExpr, rootLabel)
200
+ return out
201
+ }
202
+
203
+ public buildFieldForType(
204
+ type: IDL.Type,
205
+ label: string,
206
+ path: string
207
+ ): FormFieldNode {
208
+ return this.withName(path, () => type.accept(this, label)) as FormFieldNode
209
+ }
210
+
211
+ public buildTupleFieldForTypes(
212
+ types: IDL.Type[],
213
+ label: string,
214
+ path: string
215
+ ): FormFieldNode {
216
+ const tupleType = IDL.Tuple(...types)
217
+ return this.withName(path, () =>
218
+ tupleType.accept(this, label)
219
+ ) as FormFieldNode
220
+ }
221
+
222
+ public visitRecord(
223
+ t: IDL.RecordClass,
224
+ fields_: Array<[string, IDL.Type]>,
225
+ label: string
226
+ ): FormFieldNode {
227
+ const name = this.currentName()
228
+ const fields = fields_.map(
229
+ ([key, childType]) =>
230
+ this.withName(name ? `.${key}` : key, () =>
231
+ childType.accept(this, key)
232
+ ) as FormFieldNode
233
+ )
234
+ const schema = z.object(
235
+ Object.fromEntries(fields.map((field) => [field.label, field.schema]))
236
+ )
237
+
238
+ return {
239
+ type: "record",
240
+ label,
241
+ displayLabel: formatLabel(label),
242
+ name,
243
+ component: "record-container",
244
+ renderHint: COMPOUND_RENDER_HINT,
245
+ candidType: t.display?.() ?? t.name ?? "record",
246
+ fields,
247
+ defaultValue: Object.fromEntries(
248
+ fields.map((f) => [f.label, f.defaultValue])
249
+ ),
250
+ schema,
251
+ }
252
+ }
253
+
254
+ public visitTuple<T extends IDL.Type[]>(
255
+ _t: IDL.TupleClass<T>,
256
+ components: IDL.Type[],
257
+ label: string
258
+ ): FormFieldNode {
259
+ const name = this.currentName()
260
+ const fields = components.map(
261
+ (childType, index) =>
262
+ this.withName(`[${index}]`, () =>
263
+ childType.accept(this, String(index))
264
+ ) as FormFieldNode
265
+ )
266
+ const schema =
267
+ fields.length === 0
268
+ ? (z.tuple([]) as unknown as z.ZodTuple<
269
+ [z.ZodTypeAny, ...z.ZodTypeAny[]]
270
+ >)
271
+ : z.tuple(
272
+ fields.map((field) => field.schema) as [
273
+ z.ZodTypeAny,
274
+ ...z.ZodTypeAny[],
275
+ ]
276
+ )
277
+
278
+ return {
279
+ type: "tuple",
280
+ label,
281
+ displayLabel: formatLabel(label),
282
+ name,
283
+ component: "tuple-container",
284
+ renderHint: COMPOUND_RENDER_HINT,
285
+ candidType: "tuple",
286
+ fields,
287
+ defaultValue: fields.map((f) => f.defaultValue),
288
+ schema,
289
+ }
290
+ }
291
+
292
+ public visitVariant(
293
+ t: IDL.VariantClass,
294
+ fields_: Array<[string, IDL.Type]>,
295
+ label: string
296
+ ): FormFieldNode {
297
+ const name = this.currentName()
298
+ const options = fields_.map(
299
+ ([key, childType]) =>
300
+ this.withName(`.${key}`, () =>
301
+ childType.accept(this, key)
302
+ ) as FormFieldNode
303
+ )
304
+
305
+ const first =
306
+ options[0] ??
307
+ this.primitive("null", "null", `${name}.null`, "null", null, z.null())
308
+ const defaultOption = first.label
309
+ const variantSchemas = options.map((option) =>
310
+ option.type === "null"
311
+ ? z.object({ _type: z.literal(option.label) })
312
+ : z.object({
313
+ _type: z.literal(option.label),
314
+ [option.label]: option.schema,
315
+ })
316
+ )
317
+
318
+ const getOption = (option: string): FormFieldNode => {
319
+ const found = options.find((o) => o.label === option)
320
+ if (!found) {
321
+ throw new Error(`Unknown variant option: ${option}`)
322
+ }
323
+ return found
324
+ }
325
+
326
+ const getOptionDefault = (option: string): Record<string, unknown> => {
327
+ const field = getOption(option)
328
+ return field.type === "null"
329
+ ? { _type: option }
330
+ : { _type: option, [option]: field.defaultValue }
331
+ }
332
+
333
+ const getSelectedKey = (value: Record<string, unknown>): string => {
334
+ if (typeof value?._type === "string") return value._type
335
+ const firstPresent = Object.keys(value ?? {}).find((k) =>
336
+ options.some((o) => o.label === k)
337
+ )
338
+ return firstPresent ?? defaultOption
339
+ }
340
+
341
+ const getSelectedOption = (value: Record<string, unknown>): FormFieldNode =>
342
+ getOption(getSelectedKey(value))
343
+
344
+ return {
345
+ type: "variant",
346
+ label,
347
+ displayLabel: formatLabel(label),
348
+ name,
349
+ component: "variant-select",
350
+ renderHint: COMPOUND_RENDER_HINT,
351
+ candidType: t.display?.() ?? t.name ?? "variant",
352
+ options,
353
+ defaultOption,
354
+ defaultValue: getOptionDefault(defaultOption),
355
+ schema:
356
+ variantSchemas.length === 0
357
+ ? z.object({ _type: z.literal(defaultOption) })
358
+ : z.union(
359
+ variantSchemas as unknown as [z.ZodTypeAny, ...z.ZodTypeAny[]]
360
+ ),
361
+ getOptionDefault,
362
+ getOption,
363
+ getSelectedKey,
364
+ getSelectedOption,
365
+ }
366
+ }
367
+
368
+ public visitOpt<T>(
369
+ _t: IDL.OptClass<T>,
370
+ ty: IDL.Type<T>,
371
+ label: string
372
+ ): FormFieldNode {
373
+ const name = this.currentName()
374
+ const innerField = ty.accept(this, label) as FormFieldNode
375
+
376
+ return {
377
+ type: "optional",
378
+ label,
379
+ displayLabel: formatLabel(label),
380
+ name,
381
+ component: "optional-toggle",
382
+ renderHint: COMPOUND_RENDER_HINT,
383
+ candidType: `opt ${innerField.candidType}`,
384
+ innerField,
385
+ defaultValue: null,
386
+ schema: z.union([
387
+ innerField.schema,
388
+ z.null(),
389
+ z.undefined().transform(() => null),
390
+ ]),
391
+ getInnerDefault: () => innerField.defaultValue,
392
+ isEnabled: (value: unknown) => value !== null && value !== undefined,
393
+ }
394
+ }
395
+
396
+ public visitVec<T>(
397
+ _t: IDL.VecClass<T>,
398
+ ty: IDL.Type<T>,
399
+ label: string
400
+ ): FormFieldNode {
401
+ const name = this.currentName()
402
+
403
+ if (ty instanceof IDL.FixedNatClass && ty._bits === 8) {
404
+ return this.primitive(
405
+ "blob",
406
+ label,
407
+ name,
408
+ "blob",
409
+ "",
410
+ z.union([z.string(), z.array(z.number()), z.instanceof(Uint8Array)])
411
+ )
412
+ }
413
+
414
+ const itemFieldTemplate = this.withName("[0]", () =>
415
+ ty.accept(this, `${label}_item`)
416
+ ) as FormFieldNode
417
+
418
+ const createItemField = (index: number, overrides?: { label?: string }) => {
419
+ return this.withName(`[${index}]`, () =>
420
+ ty.accept(this, overrides?.label ?? String(index))
421
+ ) as FormFieldNode
422
+ }
423
+
424
+ return {
425
+ type: "vector",
426
+ label,
427
+ displayLabel: formatLabel(label),
428
+ name,
429
+ component: "vector-list",
430
+ renderHint: COMPOUND_RENDER_HINT,
431
+ candidType: `vec ${ty.display?.() ?? ty.name}`,
432
+ itemField: itemFieldTemplate,
433
+ defaultValue: [],
434
+ schema: z.array(itemFieldTemplate.schema),
435
+ getItemDefault: () => cloneField(itemFieldTemplate).defaultValue,
436
+ createItemField,
437
+ }
438
+ }
439
+
440
+ public visitRec<T>(
441
+ t: IDL.RecClass<T>,
442
+ ty: IDL.ConstructType<T>,
443
+ label: string
444
+ ): FormFieldNode {
445
+ const name = this.currentName()
446
+ const typeName = ty.name || "RecursiveType"
447
+ let schema: z.ZodTypeAny
448
+
449
+ if (this.recCache.has(t)) {
450
+ return this.recCache.get(t)!
451
+ }
452
+
453
+ if (this.recursiveSchemas.has(typeName)) {
454
+ schema = this.recursiveSchemas.get(typeName)!
455
+ } else {
456
+ schema = z.lazy(() => (ty.accept(this, label) as FormFieldNode).schema)
457
+ this.recursiveSchemas.set(typeName, schema)
458
+ }
459
+
460
+ const node: FormFieldNode = {
461
+ type: "recursive",
462
+ label,
463
+ displayLabel: formatLabel(label),
464
+ name,
465
+ component: "recursive-lazy",
466
+ renderHint: COMPOUND_RENDER_HINT,
467
+ candidType: ty.name,
468
+ defaultValue: undefined,
469
+ schema,
470
+ typeName: ty.name,
471
+ extract: () =>
472
+ this.withName(name, () => ty.accept(this, label)) as FormFieldNode,
473
+ }
474
+
475
+ this.recCache.set(t, node)
476
+ return node
477
+ }
478
+
479
+ public visitPrincipal(_t: IDL.PrincipalClass, label: string): FormFieldNode {
480
+ return this.primitive(
481
+ "principal",
482
+ label,
483
+ this.currentName(),
484
+ "principal",
485
+ "",
486
+ z.custom<Principal>(
487
+ (val) => {
488
+ if (val instanceof Principal) return true
489
+ if (typeof val === "string") {
490
+ try {
491
+ Principal.fromText(val)
492
+ return true
493
+ } catch {
494
+ return false
495
+ }
496
+ }
497
+ return false
498
+ },
499
+ { message: "Invalid Principal format" }
500
+ )
501
+ )
502
+ }
503
+
504
+ public visitText(_t: IDL.TextClass, label: string): FormFieldNode {
505
+ return this.primitive(
506
+ "text",
507
+ label,
508
+ this.currentName(),
509
+ "text",
510
+ "",
511
+ z.string().min(1, "Required")
512
+ )
513
+ }
514
+
515
+ public visitBool(_t: IDL.BoolClass, label: string): FormFieldNode {
516
+ return this.primitive(
517
+ "boolean",
518
+ label,
519
+ this.currentName(),
520
+ "bool",
521
+ false,
522
+ z.boolean()
523
+ )
524
+ }
525
+
526
+ public visitNull(_t: IDL.NullClass, label: string): FormFieldNode {
527
+ return this.primitive(
528
+ "null",
529
+ label,
530
+ this.currentName(),
531
+ "null",
532
+ null,
533
+ z.null()
534
+ )
535
+ }
536
+
537
+ public visitInt(_t: IDL.IntClass, label: string): FormFieldNode {
538
+ return this.primitive(
539
+ "number",
540
+ label,
541
+ this.currentName(),
542
+ "int",
543
+ "",
544
+ z
545
+ .string()
546
+ .min(1, "Required")
547
+ .regex(/^-?\d+$/, "Must be an integer")
548
+ )
549
+ }
550
+
551
+ public visitNat(_t: IDL.NatClass, label: string): FormFieldNode {
552
+ return this.primitive(
553
+ "number",
554
+ label,
555
+ this.currentName(),
556
+ "nat",
557
+ "",
558
+ z.string().regex(/^\d+$/, "Must be a positive number")
559
+ )
560
+ }
561
+
562
+ public visitFloat(t: IDL.FloatClass, label: string): FormFieldNode {
563
+ return this.primitive(
564
+ "number",
565
+ label,
566
+ this.currentName(),
567
+ `float${t._bits}`,
568
+ "",
569
+ z
570
+ .string()
571
+ .min(1, "Required")
572
+ .refine((val) => !isNaN(Number(val)) && isFinite(Number(val)), {
573
+ message: "Must be a valid number",
574
+ })
575
+ )
576
+ }
577
+
578
+ public visitFixedInt(t: IDL.FixedIntClass, label: string): FormFieldNode {
579
+ return this.primitive(
580
+ "number",
581
+ label,
582
+ this.currentName(),
583
+ `int${t._bits}`,
584
+ "",
585
+ z
586
+ .string()
587
+ .min(1, "Required")
588
+ .regex(/^-?\d+$/, "Must be an integer")
589
+ )
590
+ }
591
+
592
+ public visitFixedNat(t: IDL.FixedNatClass, label: string): FormFieldNode {
593
+ return this.primitive(
594
+ "number",
595
+ label,
596
+ this.currentName(),
597
+ `nat${t._bits}`,
598
+ "",
599
+ z.string().regex(/^\d+$/, "Must be a positive number")
600
+ )
601
+ }
602
+
603
+ public visitType<T>(t: IDL.Type<T>, label: string): FormFieldNode {
604
+ return this.primitive(
605
+ "unknown",
606
+ label,
607
+ this.currentName(),
608
+ t.name ?? "unknown",
609
+ null,
610
+ z.any()
611
+ )
612
+ }
613
+
614
+ private primitive<T extends FormFieldType>(
615
+ type: T,
616
+ label: string,
617
+ name: string,
618
+ candidType: string,
619
+ defaultValue: unknown,
620
+ schema: z.ZodTypeAny
621
+ ): Extract<FormFieldNode, { type: T }> {
622
+ const component =
623
+ type === "blob"
624
+ ? "blob-upload"
625
+ : type === "principal"
626
+ ? "principal-input"
627
+ : type === "text"
628
+ ? "text-input"
629
+ : type === "number"
630
+ ? "number-input"
631
+ : type === "boolean"
632
+ ? "boolean-checkbox"
633
+ : type === "null"
634
+ ? "null-hidden"
635
+ : "unknown-fallback"
636
+
637
+ const renderHint =
638
+ type === "blob"
639
+ ? FILE_RENDER_HINT
640
+ : type === "number"
641
+ ? NUMBER_RENDER_HINT
642
+ : type === "boolean"
643
+ ? CHECKBOX_RENDER_HINT
644
+ : TEXT_RENDER_HINT
645
+
646
+ return {
647
+ type,
648
+ label,
649
+ displayLabel: formatLabel(label),
650
+ name,
651
+ component,
652
+ renderHint,
653
+ candidType,
654
+ defaultValue,
655
+ schema,
656
+ } as Extract<FormFieldNode, { type: T }>
657
+ }
658
+ }