form-driver 0.4.0 → 0.4.2

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/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "form-driver",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "An efficient framework for creating forms.",
5
5
  "license": "MIT",
6
6
  "authors": [
7
+ "曹志贤 <1546158360@qq.com>",
7
8
  "龙湖 <59775976@qq.com>",
8
9
  "云开 <2313303800@qq.com>"
9
10
  ],
@@ -136,4 +137,4 @@
136
137
  "engines": {
137
138
  "node": ">=v14.15.4"
138
139
  }
139
- }
140
+ }
@@ -1,12 +1,17 @@
1
1
  import React, { ClassType } from "react";
2
- import { MValidationResult, MFieldSchemaAnonymity, MProp, MValidationFail } from './Schema';
2
+ import {
3
+ MValidationResult,
4
+ MFieldSchemaAnonymity,
5
+ MProp,
6
+ MValidationFail,
7
+ } from "./Schema";
3
8
  import { CHANGE_SCHEMA_CALLBACK } from "../framework/Schema";
4
9
  import _ from "lodash";
5
- import { MUtil } from './MUtil';
10
+ import { MUtil } from "./MUtil";
6
11
  import { MType, PluginType } from "../types/MType";
7
12
 
8
13
  export type MORPH = "readable" | "editor";
9
- export type VIEWER = ClassType<MProp, any, any>
14
+ export type VIEWER = ClassType<MProp, any, any>;
10
15
 
11
16
  /** 统一的视觉样式 */
12
17
  export interface MTheme {
@@ -39,8 +44,8 @@ const defaultTheme: MTheme = {
39
44
  READABLE_INVALID: "❓",
40
45
  READABLE_ERROR: "❗",
41
46
 
42
- themeName: "antMiddle"
43
- }
47
+ themeName: "antMiddle",
48
+ };
44
49
 
45
50
  /**
46
51
  * 注册viewer,type,morph(viewer和type之间的关联)
@@ -49,7 +54,13 @@ export class Assembly {
49
54
  types: { [name: string]: MType } = {};
50
55
  viewers: { [name: string]: VIEWER } = {};
51
56
  editors: { [name: string]: EDITOR } = {};
52
- morph: { [name: /*MORPH*/ string]: { [typeName: string]: string | ClassType<MProp, any, any> /* viewer name or viewer */ } } = {}
57
+ morph: {
58
+ [name: /*MORPH*/ string]: {
59
+ [typeName: string]:
60
+ | string
61
+ | ClassType<MProp, any, any> /* viewer name or viewer */;
62
+ };
63
+ } = {};
53
64
 
54
65
  theme: MTheme = defaultTheme;
55
66
 
@@ -59,8 +70,13 @@ export class Assembly {
59
70
  let r;
60
71
  if (_.isString(s.toReadable)) {
61
72
  // eslint-disable-next-line no-new-func
62
- r = new Function("_", "value", "theme",
63
- "const {READABLE_UNKNOWN, READABLE_BLANK, READABLE_INVALID, READABLE_ERROR} = theme; return " + s.toReadable)(_, v, this.theme);
73
+ r = new Function(
74
+ "_",
75
+ "value",
76
+ "theme",
77
+ "const {READABLE_UNKNOWN, READABLE_BLANK, READABLE_INVALID, READABLE_ERROR} = theme; return " +
78
+ s.toReadable
79
+ )(_, v, this.theme);
64
80
  } else if (_.isFunction(s.toReadable)) {
65
81
  r = s.toReadable(v, parent, this);
66
82
  }
@@ -71,12 +87,15 @@ export class Assembly {
71
87
  }
72
88
  return r;
73
89
  } else {
74
- return s.type + "类型无效"
90
+ return s.type + "类型无效";
75
91
  }
76
- }
92
+ };
77
93
 
78
94
  /** 根据定义返回View,返回nil表示没有可用的View */
79
- getViewerOf(f: MFieldSchemaAnonymity, morph: MORPH): ClassType<any, any, any> {
95
+ getViewerOf(
96
+ f: MFieldSchemaAnonymity,
97
+ morph: MORPH
98
+ ): ClassType<any, any, any> {
80
99
  if (f.editor && morph === "editor") {
81
100
  if (_.isString(f.editor)) {
82
101
  return _.get(this.viewers, f.editor);
@@ -90,22 +109,30 @@ export class Assembly {
90
109
  return f.readable;
91
110
  }
92
111
  } else {
93
- const viewer: string | ClassType<MProp, any, any> = _.get(this.morph, morph + "." + f.type);
112
+ const viewer: string | ClassType<MProp, any, any> = _.get(
113
+ this.morph,
114
+ morph + "." + f.type
115
+ );
94
116
  if (_.isString(viewer)) {
95
117
  return _.get(this.viewers, viewer);
96
118
  } else {
97
- return viewer
119
+ return viewer;
98
120
  }
99
121
  }
100
122
  }
101
123
 
102
- validate(s: MFieldSchemaAnonymity, v: any, path: string = ""): MValidationFail | undefined {
124
+ validate(
125
+ s: MFieldSchemaAnonymity,
126
+ v: any,
127
+ path: string = ""
128
+ ): MValidationFail | undefined {
103
129
  let result: MValidationResult = undefined;
104
130
  for (let validator of this.types[s.type].validators) {
105
131
  result = validator(this, s, v, path);
106
- if (result === "pass") {
132
+
133
+ if (result === "pass" && s.type !== "weight") {
107
134
  return undefined;
108
- } else if (result) {
135
+ } else if (typeof result === "object") {
109
136
  MUtil.debug("校验", path, result.message);
110
137
  return result;
111
138
  }
@@ -115,8 +142,10 @@ export class Assembly {
115
142
 
116
143
  addViewer(name: string, v: VIEWER) {
117
144
  if (this.viewers[name]) {
118
- console.error(`addViewer: 已经存在名为 ${name} 的 Viewer,无法再次添加!`)
119
- return
145
+ console.error(
146
+ `addViewer: 已经存在名为 ${name} 的 Viewer,无法再次添加!`
147
+ );
148
+ return;
120
149
  } else {
121
150
  this.viewers[name] = v;
122
151
  }
@@ -124,8 +153,10 @@ export class Assembly {
124
153
 
125
154
  addEditor(name: string, v: EDITOR) {
126
155
  if (this.editors[name]) {
127
- console.error(`addEditor: 已经存在名为 ${name} 的 Editor,无法再次添加!`)
128
- return
156
+ console.error(
157
+ `addEditor: 已经存在名为 ${name} 的 Editor,无法再次添加!`
158
+ );
159
+ return;
129
160
  } else {
130
161
  this.editors[name] = v;
131
162
  }
@@ -139,12 +170,11 @@ export class Assembly {
139
170
  * @param typeParam 类型的描述对象
140
171
  */
141
172
  addType(typeParam: PluginType) {
142
- const { name, type, editor, readable = "DivViewer" } = typeParam
173
+ const { name, type, editor, readable = "DivViewer" } = typeParam;
143
174
  this.types[name] = type;
144
-
175
+
145
176
  _.set(this.morph, "editor." + name, editor);
146
177
  _.set(this.morph, "readable." + name, readable);
147
-
148
178
  }
149
179
 
150
180
  constructor() {
@@ -34,6 +34,7 @@ import { AExperience } from "../ui/editor/complex/AExperience";
34
34
  import { ACnAddress } from "../ui/editor/complex/ACnAddress";
35
35
  import { AArray } from "../ui/editor/complex/AArray";
36
36
  import { ACheckDrag } from "../ui/editor/complex/ACheckDrag";
37
+ import { AWeight } from "../ui/editor/complex/AWeight";
37
38
  import { AArrayGrid } from "../ui/editor/complex/AArrayGrid";
38
39
  import { ARangePicker } from "../ui/editor/basic/ARangePicker";
39
40
  import { AIntDiff } from "../ui/editor/complex/AIntDiff";
@@ -48,6 +49,7 @@ import { A } from "../ui/readable/A";
48
49
  import { ADialogForm } from "../ui/editor/complex/ADialogForm";
49
50
  import { MVLPairType } from "../types/MVLPairType";
50
51
  import { MKvSetType } from "../types/MKvSetType";
52
+ import { MWeightType } from "../types/MWeightType";
51
53
  import { AKvSet } from "../ui/editor/basic/AKvSet";
52
54
  import { ACascadePicker } from "../ui/editor/basic/ACascadePicker";
53
55
  import { MCascadeType } from "../types/MCascadeType";
@@ -74,6 +76,7 @@ export function ensureM3() {
74
76
  yearMonth: MDateTimeType,
75
77
  yearMonthDay: MDateTimeType,
76
78
  set: MSetType,
79
+ weight: MWeightType,
77
80
  array: MArrayType,
78
81
  string: MStringType,
79
82
  intDiff: MIntDiffType,
@@ -127,6 +130,7 @@ export function ensureM3() {
127
130
  ADialogForm: ADialogForm,
128
131
  AKvSet: AKvSet,
129
132
  ACascadePicker: ACascadePicker,
133
+ AWeight: AWeight,
130
134
  });
131
135
 
132
136
  assembly.morph = _.merge(assembly.morph, {
@@ -102,7 +102,7 @@ const M3 = (props: React.PropsWithChildren<M3Prop & { debug?: boolean }>) => {
102
102
 
103
103
  return debug ? (
104
104
  <MViewerDebug
105
- key={k}
105
+ // key={k}
106
106
  {...props}
107
107
  database={database}
108
108
  schema={schema}
@@ -111,7 +111,7 @@ const M3 = (props: React.PropsWithChildren<M3Prop & { debug?: boolean }>) => {
111
111
  />
112
112
  ) : (
113
113
  <MViewer
114
- key={k}
114
+ // key={k}
115
115
  {...props}
116
116
  database={database}
117
117
  schema={schema}
@@ -68,14 +68,14 @@ interface State {
68
68
  * 一个完整的表单
69
69
  */
70
70
  export class MViewer extends React.Component<MViewerProp, State> {
71
- database: any;
72
-
71
+ database: any = {};
73
72
  constructor(p: MViewerProp) {
74
73
  super(p);
75
74
  this.state = {
76
75
  forceValid: false,
77
76
  ctrlVersion: 1,
78
77
  };
78
+ console.log("执行 constructor");
79
79
 
80
80
  ensureM3();
81
81
 
@@ -95,6 +95,32 @@ export class MViewer extends React.Component<MViewerProp, State> {
95
95
  this.recover();
96
96
  }
97
97
 
98
+ // ⚠️ 新增:监听 props 变化
99
+ componentDidUpdate(prevProps: MViewerProp) {
100
+ console.log("MViewer: componentDidUpdate");
101
+ // 检查 schema 是否变化
102
+ if (!_.isEqual(prevProps.schema, this.props.schema)) {
103
+ console.log("MViewer: schema changed", {
104
+ prevSchema: prevProps.schema,
105
+ nextSchema: this.props.schema,
106
+ });
107
+
108
+ // 重新初始化 database
109
+ this.database = assembly.types[this.props.schema.type]?.standardValue(
110
+ assembly,
111
+ this.props.schema,
112
+ this.props.database,
113
+ false
114
+ );
115
+
116
+ // 填入默认值
117
+ MUtil.applyDefaultValue(this.props.schema, this.props.database, "");
118
+
119
+ // 触发重新渲染
120
+ this.setState({ ctrlVersion: this.state.ctrlVersion + 1 });
121
+ }
122
+ }
123
+
98
124
  recover() {
99
125
  const { ctrlVersion } = this.state;
100
126
  const { persistant } = this.props;
@@ -109,11 +135,13 @@ export class MViewer extends React.Component<MViewerProp, State> {
109
135
  const props = this.props;
110
136
  const database = this.database;
111
137
  const { ctrlVersion, forceValid } = this.state;
112
-
113
138
  return (
114
139
  <MContext.Provider
115
140
  value={{
116
- rootProps: props,
141
+ rootProps: {
142
+ ...props,
143
+ database,
144
+ },
117
145
  forceValid,
118
146
  setForceValid: (b) => {
119
147
  this.setState({ forceValid: true });
@@ -191,10 +219,6 @@ export function SubmitBar(props: {
191
219
  ctx.rootProps.schema,
192
220
  ctx.rootProps.database
193
221
  );
194
- // console.log("当前数据格式", {
195
- // schema: ctx.rootProps.schema,
196
- // database: ctx.rootProps.database,
197
- // });
198
222
  const submit = props.onSubmit ?? ctx.rootProps.onSubmit;
199
223
  ctx.setForceValid(true);
200
224
  if (r) {
@@ -1,32 +1,46 @@
1
1
  import _ from "lodash";
2
- import { Assembly, assembly } from './Assembly';
3
- import { MFieldSchemaAnonymity, MValidationResult} from './Schema';
2
+ import { Assembly, assembly } from "./Assembly";
3
+ import { MFieldSchemaAnonymity, MValidationResult } from "./Schema";
4
4
 
5
- export type VALIDATOR = (a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string)=> MValidationResult;
5
+ export type VALIDATOR = (
6
+ a: Assembly,
7
+ schema: MFieldSchemaAnonymity,
8
+ value: any,
9
+ path: string
10
+ ) => MValidationResult;
6
11
 
7
12
  /**
8
13
  * 非空校验,数据不能是null/undefined/""/NaN/[]
9
14
  * 要在其他条件之前,以便required=false时短路掉nil的数据,否则后面的校验全都得处理nil
10
- * @param a
11
- * @param schema
12
- * @param value
13
- * @param path
14
- * @returns
15
+ * @param a
16
+ * @param schema
17
+ * @param value
18
+ * @param path
19
+ * @returns
15
20
  */
16
- export function validateRequired(a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
17
- if(schema.required){
18
- console.log('validateRequired-value', path, value)
19
- if(_.isNil(value) || value === "" || _.isNaN(value) || (_.isArray(value) && value.length == 0)) {
20
- return {message:'您还没有填完这一项', path};
21
+ export function validateRequired(
22
+ a: Assembly,
23
+ schema: MFieldSchemaAnonymity,
24
+ value: any,
25
+ path: string
26
+ ): MValidationResult {
27
+ if (schema.required) {
28
+ if (
29
+ _.isNil(value) ||
30
+ value === "" ||
31
+ _.isNaN(value) ||
32
+ (_.isArray(value) && value.length == 0)
33
+ ) {
34
+ return { message: "您还没有填完这一项", path };
21
35
  }
22
36
  // 凡是总有例外
23
- if(schema.type === "set" && schema.openOption) {
24
- if(value.length === 1 && !value[0]){
25
- return {message:'您还没有填完这一项', path}; // 开放set,只勾了开放选项,没有填内容,也要算空
37
+ if (schema.type === "set" && schema.openOption) {
38
+ if (value.length === 1 && !value[0]) {
39
+ return { message: "您还没有填完这一项", path }; // 开放set,只勾了开放选项,没有填内容,也要算空
26
40
  }
27
41
  }
28
42
  } else {
29
- if(_.isNil(value)) {
43
+ if (_.isNil(value)) {
30
44
  return "pass";
31
45
  }
32
46
  }
@@ -34,22 +48,32 @@ export function validateRequired(a:Assembly, schema:MFieldSchemaAnonymity, value
34
48
  }
35
49
 
36
50
  /** 和validateRequired相同,但不短路 */
37
- export function validateRequiredNS(a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
51
+ export function validateRequiredNS(
52
+ a: Assembly,
53
+ schema: MFieldSchemaAnonymity,
54
+ value: any,
55
+ path: string
56
+ ): MValidationResult {
38
57
  const v = validateRequired(a, schema, value, path);
39
- if(v === "pass"){
58
+ if (v === "pass") {
40
59
  return undefined;
41
60
  }
42
61
  }
43
62
 
44
- export function validateDateMinMax(a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
45
- if(schema.min){
46
- if(!value || value < schema.min) {
47
- return {message: `请选择${schema.min}之后的时间`, path};
63
+ export function validateDateMinMax(
64
+ a: Assembly,
65
+ schema: MFieldSchemaAnonymity,
66
+ value: any,
67
+ path: string
68
+ ): MValidationResult {
69
+ if (schema.min) {
70
+ if (!value || value < schema.min) {
71
+ return { message: `请选择${schema.min}之后的时间`, path };
48
72
  }
49
73
  }
50
- if(schema.max){
51
- if(!value || value > schema.max) {
52
- return {message: `请选择${schema.min}之前的时间`, path};
74
+ if (schema.max) {
75
+ if (!value || value > schema.max) {
76
+ return { message: `请选择${schema.min}之前的时间`, path };
53
77
  }
54
78
  }
55
79
  return undefined;
@@ -60,15 +84,20 @@ export function validateDateMinMax(a:Assembly, schema:MFieldSchemaAnonymity, val
60
84
  * @param schema
61
85
  * @param value 应该是个数组
62
86
  */
63
- export function validateArrayItemMinMax(a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
64
- if(schema.min){
65
- if(!value || value.length < schema.min) {
66
- return {message: `至少选择${schema.min}项`, path};
87
+ export function validateArrayItemMinMax(
88
+ a: Assembly,
89
+ schema: MFieldSchemaAnonymity,
90
+ value: any,
91
+ path: string
92
+ ): MValidationResult {
93
+ if (schema.min) {
94
+ if (!value || value.length < schema.min) {
95
+ return { message: `至少选择${schema.min}项`, path };
67
96
  }
68
97
  }
69
- if(schema.max){
70
- if(!value || value.length > schema.max) {
71
- return {message: `最多选择${schema.max}项`, path};
98
+ if (schema.max) {
99
+ if (!value || value.length > schema.max) {
100
+ return { message: `最多选择${schema.max}项`, path };
72
101
  }
73
102
  }
74
103
  return undefined;
@@ -79,15 +108,20 @@ export function validateArrayItemMinMax(a:Assembly, schema:MFieldSchemaAnonymity
79
108
  * @param schema
80
109
  * @param value 应该是个字符串
81
110
  */
82
- export function validateStringMinMax(a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
83
- if(schema.min){
84
- if(!value || value.length < schema.min) {
85
- return {message: `至少${schema.min}个字`, path};
111
+ export function validateStringMinMax(
112
+ a: Assembly,
113
+ schema: MFieldSchemaAnonymity,
114
+ value: any,
115
+ path: string
116
+ ): MValidationResult {
117
+ if (schema.min) {
118
+ if (!value || value.length < schema.min) {
119
+ return { message: `至少${schema.min}个字`, path };
86
120
  }
87
121
  }
88
- if(schema.max){
89
- if(value && value.length > schema.max) {
90
- return {message: `最多${schema.max}个字`, path};
122
+ if (schema.max) {
123
+ if (value && value.length > schema.max) {
124
+ return { message: `最多${schema.max}个字`, path };
91
125
  }
92
126
  }
93
127
  return undefined;
@@ -98,29 +132,42 @@ export function validateStringMinMax(a:Assembly, schema:MFieldSchemaAnonymity, v
98
132
  * @param schema
99
133
  * @param value 应该是个数字
100
134
  */
101
- export function validateNumberMinMax(a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
102
- const temp = Number(value)
103
- if(!_.isNil(schema.max)){
104
- if(!_.isFinite(temp) || temp > schema.max) {
105
- return {message: `最多${schema.max}`, path};
135
+ export function validateNumberMinMax(
136
+ a: Assembly,
137
+ schema: MFieldSchemaAnonymity,
138
+ value: any,
139
+ path: string
140
+ ): MValidationResult {
141
+ const temp = Number(value);
142
+ if (!_.isNil(schema.max)) {
143
+ if (!_.isFinite(temp) || temp > schema.max) {
144
+ return { message: `最多${schema.max}`, path };
106
145
  }
107
146
  }
108
- if(!_.isNil(schema.min)){
109
- if(!_.isFinite(temp) || temp < schema.min) {
110
- return {message: `最少${schema.min}`, path};
147
+ if (!_.isNil(schema.min)) {
148
+ if (!_.isFinite(temp) || temp < schema.min) {
149
+ return { message: `最少${schema.min}`, path };
111
150
  }
112
151
  }
113
152
  return undefined;
114
153
  }
115
154
 
116
- export function generateRegexValidate(regex:RegExp, mismatchMsg:string):VALIDATOR {
117
- return function (a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
155
+ export function generateRegexValidate(
156
+ regex: RegExp,
157
+ mismatchMsg: string
158
+ ): VALIDATOR {
159
+ return function (
160
+ a: Assembly,
161
+ schema: MFieldSchemaAnonymity,
162
+ value: any,
163
+ path: string
164
+ ): MValidationResult {
118
165
  const asstr = _.toString(value);
119
- if( !regex.test(asstr) ){
120
- return {message: mismatchMsg, path};
166
+ if (!regex.test(asstr)) {
167
+ return { message: mismatchMsg, path };
121
168
  }
122
169
  return undefined;
123
- }
170
+ };
124
171
  }
125
172
 
126
173
  /**
@@ -128,27 +175,43 @@ export function generateRegexValidate(regex:RegExp, mismatchMsg:string):VALIDATO
128
175
  * 可以用Object.prototype.toString.call(<预期的类型>)来查看你想要的数据类型的expectType
129
176
  * @param expectType 预期类型,例如:"[object Object]" "[object Null]" "[object Number]" "[object Date]" "[object Array]"
130
177
  * @param mismatchMsg 如果不匹配,提示的错误信息
131
- * @returns
178
+ * @returns
132
179
  */
133
- export function generateJsPrototypeValidate(expectType:string, mismatchMsg:string = "数据有误,请重填此项"):VALIDATOR {
134
- return function (a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
135
- if(Object.prototype.toString.call(value) == expectType){
136
- return undefined
180
+ export function generateJsPrototypeValidate(
181
+ expectType: string,
182
+ mismatchMsg: string = "数据有误,请重填此项"
183
+ ): VALIDATOR {
184
+ return function (
185
+ a: Assembly,
186
+ schema: MFieldSchemaAnonymity,
187
+ value: any,
188
+ path: string
189
+ ): MValidationResult {
190
+ if (Object.prototype.toString.call(value) == expectType) {
191
+ return undefined;
137
192
  } else {
138
- return {message: mismatchMsg, path};
193
+ return { message: mismatchMsg, path };
139
194
  }
140
- }
195
+ };
141
196
  }
142
197
 
143
- export function generateSchemaValidate(valueSchema:MFieldSchemaAnonymity, mismatchMsg?:string):VALIDATOR {
144
- return function (a:Assembly, schema:MFieldSchemaAnonymity, value:any, path:string): MValidationResult {
145
- const r = assembly.validate(valueSchema,value, "");
146
- if(!r){
198
+ export function generateSchemaValidate(
199
+ valueSchema: MFieldSchemaAnonymity,
200
+ mismatchMsg?: string
201
+ ): VALIDATOR {
202
+ return function (
203
+ a: Assembly,
204
+ schema: MFieldSchemaAnonymity,
205
+ value: any,
206
+ path: string
207
+ ): MValidationResult {
208
+ const r = assembly.validate(valueSchema, value, "");
209
+ if (!r) {
147
210
  return r;
148
211
  } else {
149
- return {message: mismatchMsg ?? r.message, path};
212
+ return { message: mismatchMsg ?? r.message, path };
150
213
  }
151
- }
214
+ };
152
215
  }
153
216
 
154
217
  export default {
@@ -157,5 +220,5 @@ export default {
157
220
  validateDateMinMax,
158
221
  validateStringMinMax,
159
222
  validateNumberMinMax,
160
- generateRegexValidate
161
- }
223
+ generateRegexValidate,
224
+ };
@@ -18,7 +18,7 @@ function validateCandidate(
18
18
  ): MValidationResult {
19
19
  let fs = MUtil.option(schema);
20
20
  const openOption = _.clone(schema.openOption ?? schema.setOpen);
21
-
21
+ if (!value) return undefined;
22
22
  for (let v of value) {
23
23
  let vIsOk = false;
24
24
  for (let f of fs) {