reactivated 0.30.2 → 0.31.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.
@@ -1,4 +1,4 @@
1
- import produce, {castDraft} from "immer";
1
+ import {produce, castDraft} from "immer";
2
2
  import {FileInfoResult} from "prettier";
3
3
  import React from "react";
4
4
 
@@ -79,6 +79,7 @@ export interface FormHandler<T extends FieldMap> {
79
79
  nonFieldErrors: string[] | null;
80
80
 
81
81
  setValue: <K extends keyof T>(name: K, value: FormValues<T>[K]) => void;
82
+ setValues: (values: FormValues<T>) => void;
82
83
  setErrors: (errors: FormErrors<T>) => void;
83
84
  iterate: (
84
85
  iterator: Array<Extract<keyof T, string>>,
@@ -116,16 +117,15 @@ export const getInitialFormState = <T extends FieldMap>(form: FormLike<T>) => {
116
117
 
117
118
  export const getInitialFormSetState = <T extends FieldMap>(
118
119
  forms: Array<FormLike<T>>,
120
+ initial?: FormValues<T>[],
119
121
  ) => {
120
- return Object.fromEntries(
121
- forms.map((form) => [form.prefix, getInitialFormState(form)] as const),
122
- );
122
+ return forms.map((form, index) => initial?.[index] ?? getInitialFormState(form));
123
123
  };
124
124
 
125
125
  export const getInitialFormSetErrors = <T extends FieldMap>(
126
126
  forms: Array<FormLike<T>>,
127
127
  ) => {
128
- return Object.fromEntries(forms.map((form) => [form.prefix, form.errors] as const));
128
+ return forms.map((form, index) => form.errors);
129
129
  };
130
130
 
131
131
  export const getFormHandler = <T extends FieldMap>({
@@ -311,6 +311,9 @@ export const getFormHandler = <T extends FieldMap>({
311
311
  iterate,
312
312
  reset,
313
313
  setErrors,
314
+ setValues: (values) => {
315
+ setValues(() => values);
316
+ },
314
317
  setValue: (fieldName, value) => {
315
318
  changeValues(fieldName, (prevValues) => ({
316
319
  ...prevValues,
@@ -320,23 +323,34 @@ export const getFormHandler = <T extends FieldMap>({
320
323
  };
321
324
  };
322
325
 
323
- export const useForm = <T extends FieldMap>({
324
- form,
325
- ...options
326
- }: {
326
+ export const useForm = <
327
+ T extends FieldMap,
328
+ S extends Array<keyof T> = [],
329
+ R extends {[P in Exclude<keyof T, S[number]>]: T[P]} = {
330
+ [P in Exclude<keyof T, S[number]>]: T[P];
331
+ },
332
+ >(options: {
327
333
  form: FormLike<T>;
334
+ initial?: Partial<FormValues<R>>;
335
+ exclude?: [...S];
328
336
  fieldInterceptor?: (
329
- fieldName: keyof T,
330
- field: FieldHandler<T[keyof T]["widget"]>,
331
- values: FormValues<T>,
337
+ fieldName: keyof R,
338
+ field: FieldHandler<R[keyof R]["widget"]>,
339
+ values: FormValues<R>,
332
340
  ) => typeof field;
333
341
  changeInterceptor?: (
334
- name: keyof T,
335
- prevValues: FormValues<T>,
336
- nextValues: FormValues<T>,
337
- ) => FormValues<T>;
338
- }): FormHandler<T> => {
339
- const initial = getInitialFormState(form);
342
+ name: keyof R,
343
+ prevValues: FormValues<R>,
344
+ nextValues: FormValues<R>,
345
+ ) => FormValues<R>;
346
+ }): FormHandler<R> => {
347
+ const form = {
348
+ ...(options.form as any as FormLike<R>),
349
+ iterator: options.form.iterator.filter(
350
+ (field) => options.exclude == null || !options.exclude.includes(field),
351
+ ),
352
+ } as any as FormLike<R>;
353
+ const initial = {...getInitialFormState(form), ...options.initial};
340
354
  const [values, formSetValues] = React.useState(initial);
341
355
  const [errors, setErrors] = React.useState(form.errors);
342
356
 
@@ -492,11 +506,19 @@ export const Widget = (props: {field: FieldHandler<widgets.CoreWidget>}) => {
492
506
  onChange={field.handler}
493
507
  />
494
508
  );
509
+ } else if (field.tag === "django.forms.widgets.PasswordInput") {
510
+ return (
511
+ <widgets.TextInput
512
+ name={field.name}
513
+ value={field.value}
514
+ onChange={field.handler}
515
+ type="password"
516
+ />
517
+ );
495
518
  } else if (
496
519
  field.tag === "django.forms.widgets.TextInput" ||
497
520
  field.tag === "django.forms.widgets.DateInput" ||
498
521
  field.tag === "django.forms.widgets.URLInput" ||
499
- field.tag === "django.forms.widgets.PasswordInput" ||
500
522
  field.tag === "django.forms.widgets.EmailInput" ||
501
523
  field.tag === "django.forms.widgets.TimeInput" ||
502
524
  field.tag === "django.forms.widgets.NumberInput"
@@ -506,6 +528,7 @@ export const Widget = (props: {field: FieldHandler<widgets.CoreWidget>}) => {
506
528
  name={field.name}
507
529
  value={field.value}
508
530
  onChange={field.handler}
531
+ placeholder={field.widget.attrs.placeholder}
509
532
  />
510
533
  );
511
534
  } else if (field.tag === "django.forms.widgets.Select") {
@@ -573,6 +596,7 @@ export const ManagementForm = <T extends FieldMap>({
573
596
  export const useFormSet = <T extends FieldMap>(options: {
574
597
  formSet: FormSetLike<T>;
575
598
  onAddForm?: (form: FormLike<T>) => void;
599
+ initial?: FormValues<T>[];
576
600
  fieldInterceptor?: (
577
601
  fieldName: keyof T,
578
602
  field: FieldHandler<T[keyof T]["widget"]>,
@@ -584,14 +608,40 @@ export const useFormSet = <T extends FieldMap>(options: {
584
608
  nextValues: FormValues<T>,
585
609
  ) => FormValues<T>;
586
610
  }) => {
587
- const [formSet, setFormSet] = React.useState(options.formSet);
611
+ const createForm = (index: number) => {
612
+ return produce(options.formSet.empty_form, (draftState) => {
613
+ for (const fieldName of draftState.iterator) {
614
+ const prefix = `${options.formSet.prefix}-${index}`;
615
+ const field = draftState.fields[fieldName];
616
+ const htmlName = `${prefix}-${field.name}`;
617
+ draftState.fields[fieldName].widget.name = htmlName;
618
+ draftState.fields[fieldName].widget.attrs.id = `id_${htmlName}`;
619
+ draftState.prefix = prefix;
620
+ }
621
+ });
622
+ };
623
+
624
+ const formSetFromInitialValues = produce(options.formSet, (draftState) => {
625
+ if (options.initial == null) {
626
+ return;
627
+ }
628
+ draftState.total_form_count = options.initial.length;
629
+ draftState.forms = options.initial.map((_, index) => {
630
+ return castDraft(createForm(index));
631
+ });
632
+ });
633
+
634
+ const [formSet, setFormSet] = React.useState(formSetFromInitialValues);
588
635
 
589
- const initialFormSetState = getInitialFormSetState(options.formSet.forms);
636
+ const initialFormSetState = getInitialFormSetState(
637
+ formSetFromInitialValues.forms,
638
+ options.initial,
639
+ );
590
640
  const initialFormSetErrors = getInitialFormSetErrors(options.formSet.forms);
591
641
  const [values, formSetSetValues] =
592
- React.useState<Partial<typeof initialFormSetState>>(initialFormSetState);
642
+ React.useState<typeof initialFormSetState>(initialFormSetState);
593
643
  const [errors, formSetSetErrors] =
594
- React.useState<Partial<typeof initialFormSetErrors>>(initialFormSetErrors);
644
+ React.useState<typeof initialFormSetErrors>(initialFormSetErrors);
595
645
 
596
646
  const emptyFormValues = getInitialFormState(formSet.empty_form);
597
647
 
@@ -600,24 +650,23 @@ export const useFormSet = <T extends FieldMap>(options: {
600
650
  form,
601
651
  changeInterceptor: options.changeInterceptor,
602
652
  fieldInterceptor: options.fieldInterceptor,
603
- values: values[form.prefix] ?? emptyFormValues,
604
- errors: errors[form.prefix] ?? {},
653
+ values: values[index] ?? emptyFormValues,
654
+ errors: errors[index] ?? {},
605
655
  setErrors: (nextErrors) => {
606
656
  formSetSetErrors((prevErrors) => ({
607
657
  ...prevErrors,
608
- [form.prefix]: nextErrors,
658
+ [index]: nextErrors,
609
659
  }));
610
660
  },
611
661
  initial: initialFormSetState[index] ?? emptyFormValues,
612
662
  setValues: (getValuesToSetFromPrevValues) => {
613
663
  formSetSetValues((prevValues) => {
614
664
  const nextValues = getValuesToSetFromPrevValues(
615
- prevValues[form.prefix] ?? emptyFormValues,
665
+ prevValues[index] ?? emptyFormValues,
616
666
  );
617
- return {
618
- ...prevValues,
619
- [form.prefix]: nextValues,
620
- };
667
+ return produce(prevValues, (draftState) => {
668
+ draftState[index] = castDraft(nextValues);
669
+ });
621
670
  });
622
671
  },
623
672
  });
@@ -625,18 +674,9 @@ export const useFormSet = <T extends FieldMap>(options: {
625
674
 
626
675
  const addForm = () => {
627
676
  const {total_form_count} = formSet;
628
- type AdditionalForm = (typeof formSet)["forms"][number];
629
677
 
630
- const extraForm = produce(formSet.empty_form, (draftState) => {
631
- for (const fieldName of draftState.iterator) {
632
- const prefix = `${formSet.prefix}-${formSet.total_form_count}`;
633
- const field = draftState.fields[fieldName];
634
- const htmlName = `${prefix}-${field.name}`;
635
- draftState.fields[fieldName].widget.name = htmlName;
636
- draftState.fields[fieldName].widget.attrs.id = `id_${htmlName}`;
637
- draftState.prefix = prefix;
638
- }
639
- });
678
+ const extraForm = createForm(formSet.total_form_count);
679
+
640
680
  const updated = produce(formSet, (draftState) => {
641
681
  draftState.forms.push(castDraft(extraForm));
642
682
  draftState.total_form_count += 1;
@@ -646,7 +686,38 @@ export const useFormSet = <T extends FieldMap>(options: {
646
686
  options.onAddForm?.(extraForm);
647
687
  };
648
688
 
649
- return {schema: formSet, values, forms, addForm};
689
+ const clear = () => {
690
+ setFormSet(
691
+ produce(formSet, (draftState) => {
692
+ formSetSetValues([]);
693
+ draftState.forms = [];
694
+ draftState.total_form_count = 0;
695
+ }),
696
+ );
697
+ };
698
+
699
+ const setFormsAndValues = (values: FormValues<T>[]) => {
700
+ formSetSetValues(values);
701
+
702
+ const updated = produce(formSet, (draftState) => {
703
+ draftState.total_form_count = values.length;
704
+ draftState.forms = values.map((_, index) => {
705
+ return castDraft(createForm(index));
706
+ });
707
+ });
708
+ setFormSet(updated);
709
+ };
710
+
711
+ return {
712
+ schema: formSet,
713
+ values,
714
+ forms,
715
+ addForm,
716
+ clear,
717
+ setFormsAndValues,
718
+ setErrors: formSetSetErrors,
719
+ setValues: formSetSetValues,
720
+ };
650
721
  };
651
722
 
652
723
  export const bindWidgetType = <W extends WidgetLike>() => {
@@ -767,3 +838,43 @@ export const FormSet = <T extends FieldMap<widgets.CoreWidget>>(props: {
767
838
  </>
768
839
  );
769
840
  };
841
+
842
+ export type UnknownFormValues<T extends FieldMap> = {
843
+ [K in keyof T]: T[K] extends {enum: unknown} ? T[K]["enum"] | null : unknown;
844
+ };
845
+
846
+ // TODO: Should be T extends Record<string, FormLike<any> | FormSetLike<any>>
847
+ // but jsonschema outputs interfaces instead of types. Figure out how to output a type.
848
+ export type FormOrFormSetValues<T> = T extends {tag: "FormGroup"}
849
+ ? Omit<{[K in keyof T]: FormOrFormSetValues<T[K]>}, "tag">
850
+ : T extends FormLike<any>
851
+ ? UnknownFormValues<T["fields"]>
852
+ : T extends FormSetLike<any>
853
+ ? Array<UnknownFormValues<T["empty_form"]["fields"]>>
854
+ : T extends null
855
+ ? null
856
+ : never;
857
+
858
+ export type FormOrFormSetErrors<T> = T extends {tag: "FormGroup"}
859
+ ? Omit<{[K in keyof T]?: FormOrFormSetErrors<T[K]>}, "tag">
860
+ : T extends FormLike<any>
861
+ ? NonNullable<T["errors"]>
862
+ : T extends FormSetLike<any>
863
+ ? Array<NonNullable<T["empty_form"]["errors"]>>
864
+ : T extends null
865
+ ? null
866
+ : never;
867
+
868
+ // Used by Joy, but unsure what this is for.
869
+ export const getValue = (optgroup: Optgroup) => {
870
+ const rawValue = optgroup[1][0].value;
871
+
872
+ if (rawValue == null) {
873
+ return "";
874
+ } else if (rawValue === true) {
875
+ return "True";
876
+ } else if (rawValue === false) {
877
+ return "False";
878
+ }
879
+ return rawValue;
880
+ };
@@ -27,12 +27,15 @@ export const TextInput = (props: {
27
27
  className?: string;
28
28
  value: string | null;
29
29
  onChange: (value: string) => void;
30
+ placeholder?: string;
31
+ type?: "text" | "password";
30
32
  }) => {
31
33
  return (
32
34
  <input
33
- type="text"
35
+ type={props.type ?? "text"}
34
36
  name={props.name}
35
37
  className={props.className}
38
+ placeholder={props.placeholder}
36
39
  value={props.value ?? ""}
37
40
  onChange={(event) => props.onChange(event.target.value)}
38
41
  />
package/src/generated.tsx CHANGED
@@ -30,6 +30,9 @@ export interface Types {
30
30
  URLSchema: {
31
31
  [k: string]: ReactivatedTypesURL;
32
32
  };
33
+ RPCRegistry: {
34
+ [k: string]: ReactivatedTypesRPC;
35
+ };
33
36
  }
34
37
  export interface DjangoFormsWidgetsHiddenInput {
35
38
  template_name: "django/forms/widgets/hidden.html";
@@ -277,3 +280,10 @@ export interface ReactivatedTypesURL {
277
280
  [k: string]: string;
278
281
  };
279
282
  }
283
+ export interface ReactivatedTypesRPC {
284
+ params: [string, string][];
285
+ url: string;
286
+ input: string | null;
287
+ output: string;
288
+ type: "form" | "form_set" | "form_group";
289
+ }
package/src/generator.mts CHANGED
@@ -10,7 +10,9 @@ const stdinBuffer = fs.readFileSync(0);
10
10
  const {compile} = await import("json-schema-to-typescript");
11
11
 
12
12
  import {
13
+ CodeBlockWriter,
13
14
  Project,
15
+ Scope,
14
16
  SourceFile,
15
17
  StructureKind,
16
18
  SyntaxKind,
@@ -20,7 +22,16 @@ import {
20
22
  } from "ts-morph";
21
23
 
22
24
  const schema = JSON.parse(stdinBuffer.toString("utf8"));
23
- const {urls: possibleEmptyUrls, templates, interfaces, types, values} = schema;
25
+ const {
26
+ urls: possibleEmptyUrls,
27
+ templates,
28
+ interfaces,
29
+ rpc: uncastedRpc,
30
+ types,
31
+ values,
32
+ } = schema;
33
+
34
+ const rpc: generated.Types["RPCRegistry"] = uncastedRpc;
24
35
 
25
36
  const urls: generated.Types["URLSchema"] = {
26
37
  ...possibleEmptyUrls,
@@ -40,6 +51,154 @@ const project = new Project();
40
51
 
41
52
  const sourceFile = project.createSourceFile("");
42
53
 
54
+ const classDeclaration = sourceFile.addClass({
55
+ name: "RPC",
56
+ isExported: true,
57
+ });
58
+
59
+ const rpcConstructor = classDeclaration.addConstructor({
60
+ parameters: [{name: "requester", type: "rpcUtils.Requester", scope: Scope.Public}],
61
+ });
62
+
63
+ let rpcConstructorBody = "";
64
+
65
+ for (const name of Object.keys(rpc)) {
66
+ const {url, input, output, type, params} = rpc[name];
67
+ const functionDeclaration = classDeclaration.addMethod({
68
+ name,
69
+ });
70
+
71
+ rpcConstructorBody += `this.${name} = (this.${name} as any).bind(this);\n`;
72
+ functionDeclaration.setIsAsync(true);
73
+
74
+ functionDeclaration.addParameter({
75
+ name: "this",
76
+ type: "void",
77
+ });
78
+
79
+ let bodyText = "";
80
+ const initializer = {
81
+ url: (writer: CodeBlockWriter) => writer.quote(url),
82
+ name: (writer: CodeBlockWriter) => writer.quote(name),
83
+ };
84
+
85
+ if (params.length >= 1) {
86
+ const paramsInterface = functionDeclaration.addInterface({
87
+ name: "WILL_BE_STRIPPED",
88
+ });
89
+
90
+ const iterator = [];
91
+ for (const [paramType, paramName] of params) {
92
+ paramsInterface.addProperty({name: paramName, type: "string | number"});
93
+ iterator.push(paramName);
94
+ }
95
+ functionDeclaration.addParameter({
96
+ name: "params",
97
+ type: paramsInterface.getText().replace("interface WILL_BE_STRIPPED", ""),
98
+ });
99
+
100
+ Object.assign(initializer, {
101
+ paramsAndIterator: Writers.object({
102
+ iterator: JSON.stringify(iterator),
103
+ params: "params",
104
+ }),
105
+ });
106
+ // Otherwise our interface will be inserted.
107
+ functionDeclaration.setBodyText("");
108
+ } else {
109
+ Object.assign(initializer, {paramsAndIterator: "null"});
110
+ }
111
+
112
+ if (input != null) {
113
+ const property = classDeclaration.addProperty({
114
+ // isStatic: true,
115
+ name: input,
116
+ type: `_Types["${input}"]`,
117
+ initializer: JSON.stringify(values[input]),
118
+ });
119
+ functionDeclaration.addParameter({
120
+ name: "input",
121
+ type: `forms.FormOrFormSetValues<_Types["${input}"]>`,
122
+ });
123
+ functionDeclaration.setReturnType(
124
+ `Promise<rpcUtils.Result<_Types["${output}"], forms.FormOrFormSetErrors<_Types["${input}"]>, _Types["RPCPermission"]>>`,
125
+ );
126
+ Object.assign(initializer, {
127
+ input: Writers.object({
128
+ values: "input",
129
+ type: (writer: CodeBlockWriter) =>
130
+ writer.quote(type).write(" as const"),
131
+ }),
132
+ });
133
+ } else {
134
+ functionDeclaration.setReturnType(
135
+ `Promise<rpcUtils.Result<_Types["${output}"], null, _Types["RPCPermission"]>>`,
136
+ );
137
+ Object.assign(initializer, {input: "null"});
138
+ }
139
+
140
+ functionDeclaration.addVariableStatement({
141
+ declarationKind: VariableDeclarationKind.Const,
142
+ declarations: [
143
+ {
144
+ name: "options",
145
+ initializer: Writers.object(initializer),
146
+ /*
147
+ x: 123,
148
+ y: (writer) => writer.quote("abc"),
149
+ z: Writers.object({
150
+ one: (writer) => writer.quote("1"),
151
+ }),
152
+ */
153
+ },
154
+ ],
155
+ });
156
+ functionDeclaration.setBodyText(
157
+ `${functionDeclaration.getBodyText()} return rpcUtils.rpcCall((this as any).requester, options)`,
158
+ );
159
+ /*
160
+
161
+ if (input != null) {
162
+ functionDeclaration.addParameter({
163
+ name: "input",
164
+ type: `forms.FormOrFormSetValues<_Types["${input}"]>`,
165
+ });
166
+ bodyText = bodyText.concat(`
167
+ const input = ${JSON.stringify({
168
+ type: "form",
169
+ })};
170
+ }
171
+ `);
172
+ }
173
+ else {
174
+ }
175
+ */
176
+
177
+ /*
178
+ if (instance.length === 1) {
179
+ functionDeclaration.addParameter({name: "instance", type: `string | number`});
180
+ functionDeclaration.setBodyText(`return rpcUtils.rpcCall("${url}", input, "${type}", instance)`);
181
+ }
182
+ else if (instance.length >= 2) {
183
+ const instanceInterface = functionDeclaration.addInterface({name: "WILL_BE_STRIPPED"});
184
+
185
+ for (const instanceArg of instance) {
186
+ instanceInterface.addProperty({name: instanceArg, type: "string | number"});
187
+ }
188
+ functionDeclaration.addParameter({name: "instance", type: instanceInterface.getText().replace("interface WILL_BE_STRIPPED", "")});
189
+ functionDeclaration.setBodyText(`const iterator = ${JSON.stringify(instance)}; return rpcUtils.rpcCall("${url}", input, "${type}", {iterator, params: instance})`);
190
+ }
191
+ else {
192
+ functionDeclaration.setBodyText(`return rpcUtils.rpcCall("${url}", input, "${type}")`);
193
+ }
194
+
195
+ functionDeclaration.addParameter({name: "input", type: `forms.FormOrFormSetValues<_Types["${input}"]>`});
196
+ functionDeclaration.setReturnType(`Promise<rpcUtils.Result<_Types["${output}"], forms.FormOrFormSetErrors<_Types["${input}"]>>>`);
197
+ functionDeclaration.setIsAsync(true);
198
+ */
199
+ }
200
+ rpcConstructor.setBodyText(rpcConstructorBody);
201
+
43
202
  if (Object.keys(urls).length !== 0) {
44
203
  sourceFile.addVariableStatement({
45
204
  declarationKind: VariableDeclarationKind.Const,
@@ -116,14 +275,18 @@ if (Object.keys(urls).length !== 0) {
116
275
  }
117
276
 
118
277
  sourceFile.addStatements(`
278
+ export const rpc = new RPC(typeof window != "undefined" ? rpcUtils.defaultRequester : null as any);
119
279
  import React from "react"
120
280
  import createContext from "reactivated/dist/context";
121
281
  import * as forms from "reactivated/dist/forms";
122
282
  import * as generated from "reactivated/dist/generated";
283
+ import * as rpcUtils from "reactivated/dist/rpc";
123
284
 
124
285
  // Note: this needs strict function types to behave correctly with excess properties etc.
125
286
  export type Checker<P, U extends (React.FunctionComponent<P> | React.ComponentClass<P>)> = {};
126
287
 
288
+ export type Result<TSuccess, TInvalid> = rpcUtils.Result<TSuccess, TInvalid, _Types["RPCPermission"]>;
289
+
127
290
  export const {Context, Provider, getServerData} = createContext<_Types["Context"]>();
128
291
 
129
292
  export const getTemplate = ({template_name}: {template_name: string}) => {
@@ -144,6 +307,7 @@ export const {createRenderer, Iterator} = forms.bindWidgetType<_Types["globals"]
144
307
  export type FieldHandler = forms.FieldHandler<_Types["globals"]["Widget"]>;
145
308
  export type models = _Types["globals"]["models"];
146
309
 
310
+ export {FormHandler} from "reactivated/dist/forms";
147
311
  export const {Form, FormSet, Widget, useForm, useFormSet, ManagementForm} = forms;
148
312
  `);
149
313
 
@@ -152,9 +316,11 @@ compile(types, "this is unused").then((ts) => {
152
316
  process.stdout.write("/* eslint-disable */\n");
153
317
  process.stdout.write(ts);
154
318
 
155
- for (const name of Object.keys(templates)) {
156
- const propsName = templates[name];
157
- sourceFile.addStatements(`
319
+ if (!fs.existsSync("./reactivated-skip-template-check")) {
320
+ for (const name of Object.keys(templates)) {
321
+ const propsName = templates[name];
322
+
323
+ sourceFile.addStatements(`
158
324
 
159
325
  import ${name}Implementation from "@client/templates/${name}"
160
326
  export type ${name}Check = Checker<_Types["${propsName}"], typeof ${name}Implementation>;
@@ -165,6 +331,7 @@ export namespace templates {
165
331
 
166
332
 
167
333
  `);
334
+ }
168
335
  }
169
336
 
170
337
  for (const name of Object.keys(interfaces)) {