@rozenite/network-activity-plugin 1.7.0 → 1.8.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 (43) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +24 -0
  3. package/dist/devtools/App.html +2 -2
  4. package/dist/devtools/assets/{App-pokLiGYV.js → App-B3xlUjs6.js} +163 -115
  5. package/dist/devtools/assets/{App-BrSkOkws.css → App-m6xge0az.css} +13 -0
  6. package/dist/react-native/chunks/boot-recording.cjs +307 -28
  7. package/dist/react-native/chunks/boot-recording.js +310 -31
  8. package/dist/react-native/chunks/useNetworkActivityDevTools.require.cjs +192 -141
  9. package/dist/react-native/chunks/useNetworkActivityDevTools.require.js +193 -142
  10. package/dist/react-native/index.d.ts +24 -7
  11. package/dist/rozenite.json +1 -1
  12. package/dist/sdk/index.cjs +127 -0
  13. package/dist/sdk/index.d.ts +1243 -0
  14. package/dist/sdk/index.js +127 -0
  15. package/package.json +17 -6
  16. package/sdk.ts +59 -0
  17. package/src/react-native/__tests__/events-listener.test.ts +35 -0
  18. package/src/react-native/agent/__tests__/network-activity-agent-state.test.ts +21 -9
  19. package/src/react-native/agent/state.ts +13 -13
  20. package/src/react-native/agent/tools.ts +20 -146
  21. package/src/react-native/agent/use-network-activity-agent-tools.ts +73 -64
  22. package/src/react-native/events-listener.ts +12 -3
  23. package/src/react-native/http/http-inspector.ts +19 -5
  24. package/src/react-native/network-inspector.ts +46 -8
  25. package/src/react-native/nitro-fetch/__tests__/nitro-network-inspector.test.ts +198 -0
  26. package/src/react-native/nitro-fetch/nitro-network-inspector.ts +403 -0
  27. package/src/react-native/useHttpInspector.ts +9 -19
  28. package/src/react-native/useNetworkActivityDevTools.ts +13 -1
  29. package/src/react-native/websocket/__tests__/websocket-inspector.test.ts +69 -0
  30. package/src/react-native/websocket/websocket-inspector.ts +32 -17
  31. package/src/shared/agent-tools.ts +230 -0
  32. package/src/shared/client.ts +3 -0
  33. package/src/shared/http-events.ts +6 -0
  34. package/src/shared/websocket-events.ts +16 -7
  35. package/src/ui/components/RequestList.tsx +21 -0
  36. package/src/ui/components/SidePanel.tsx +12 -9
  37. package/src/ui/state/derived.ts +4 -0
  38. package/src/ui/state/model.ts +6 -1
  39. package/src/ui/state/store.ts +52 -36
  40. package/src/ui/tabs/HeadersTab.tsx +18 -4
  41. package/src/ui/tabs/ResponseTab.tsx +5 -3
  42. package/tsconfig.json +4 -1
  43. package/vite.config.ts +7 -0
@@ -203,7 +203,7 @@ function getContentTypeMime(headers) {
203
203
  const actualValue = Array.isArray(value) ? value[0] : value;
204
204
  return actualValue.split(";")[0].trim();
205
205
  }
206
- const getContentType = (request) => {
206
+ const getContentType$1 = (request) => {
207
207
  const responseHeaders = request.responseHeaders;
208
208
  const responseType = request.responseType;
209
209
  const contentType = getContentTypeMime(responseHeaders || {});
@@ -363,7 +363,7 @@ const setupRequestOverride = (overridesRegistry, request) => {
363
363
  Object.defineProperty(request, "responseType", { writable: true });
364
364
  Object.defineProperty(request, "response", { writable: true });
365
365
  Object.defineProperty(request, "responseText", { writable: true });
366
- const contentType = getContentType(request);
366
+ const contentType = getContentType$1(request);
367
367
  if (contentType === "application/json") {
368
368
  request.responseType = "json";
369
369
  } else if (contentType === "text/plain") {
@@ -456,7 +456,8 @@ const getHTTPInspector = () => {
456
456
  postData: getRequestBody(data)
457
457
  },
458
458
  type: "XHR",
459
- initiator
459
+ initiator,
460
+ source: "builtin"
460
461
  });
461
462
  request.addEventListener("readystatechange", () => {
462
463
  if (request.readyState === READY_STATE_HEADERS_RECEIVED) {
@@ -469,7 +470,8 @@ const getHTTPInspector = () => {
469
470
  timestamp: Date.now(),
470
471
  loaded: event.loaded,
471
472
  total: event.total,
472
- lengthComputable: event.lengthComputable
473
+ lengthComputable: event.lengthComputable,
474
+ source: "builtin"
473
475
  });
474
476
  });
475
477
  request.addEventListener("load", () => {
@@ -484,10 +486,11 @@ const getHTTPInspector = () => {
484
486
  headers: applyReactNativeResponseHeadersLogic(
485
487
  request.responseHeaders || {}
486
488
  ),
487
- contentType: getContentType(request),
489
+ contentType: getContentType$1(request),
488
490
  size: getResponseSize(request),
489
491
  responseTime: Date.now()
490
- }
492
+ },
493
+ source: "builtin"
491
494
  });
492
495
  });
493
496
  request.addEventListener("loadend", () => {
@@ -496,7 +499,8 @@ const getHTTPInspector = () => {
496
499
  timestamp: Date.now(),
497
500
  duration: Date.now() - sendTime,
498
501
  size: getResponseSize(request),
499
- ttfb
502
+ ttfb,
503
+ source: "builtin"
500
504
  });
501
505
  });
502
506
  request.addEventListener("error", () => {
@@ -505,7 +509,8 @@ const getHTTPInspector = () => {
505
509
  timestamp: Date.now(),
506
510
  type: "XHR",
507
511
  error: "Failed",
508
- canceled: false
512
+ canceled: false,
513
+ source: "builtin"
509
514
  });
510
515
  });
511
516
  request.addEventListener("abort", () => {
@@ -514,7 +519,8 @@ const getHTTPInspector = () => {
514
519
  timestamp: Date.now(),
515
520
  type: "XHR",
516
521
  error: "Aborted",
517
- canceled: true
522
+ canceled: true,
523
+ source: "builtin"
518
524
  });
519
525
  });
520
526
  request.addEventListener("timeout", () => {
@@ -523,7 +529,8 @@ const getHTTPInspector = () => {
523
529
  timestamp: Date.now(),
524
530
  type: "XHR",
525
531
  error: "Timeout",
526
- canceled: false
532
+ canceled: false,
533
+ source: "builtin"
527
534
  });
528
535
  });
529
536
  });
@@ -580,10 +587,10 @@ const WEBSOCKET_EVENTS = [
580
587
  const isWebSocketEvent = (type) => {
581
588
  return WEBSOCKET_EVENTS.includes(type);
582
589
  };
583
- const getWebSocketInspector = () => {
590
+ const toSocketId = (socketId) => String(socketId);
591
+ const createWebSocketInspector = (webSocketInterceptor = getWebSocketInterceptor()) => {
584
592
  const eventEmitter = createNanoEvents();
585
593
  const socketUrlMap = /* @__PURE__ */ new Map();
586
- const webSocketInterceptor = getWebSocketInterceptor();
587
594
  return {
588
595
  enable: () => {
589
596
  webSocketInterceptor.setConnectCallback(
@@ -592,10 +599,11 @@ const getWebSocketInspector = () => {
592
599
  const event = {
593
600
  type: "websocket-connect",
594
601
  url,
595
- socketId,
602
+ socketId: toSocketId(socketId),
596
603
  timestamp: Date.now(),
597
604
  protocols,
598
- options
605
+ options,
606
+ source: "builtin"
599
607
  };
600
608
  eventEmitter.emit("websocket-connect", event);
601
609
  }
@@ -609,10 +617,11 @@ const getWebSocketInspector = () => {
609
617
  const event = {
610
618
  type: "websocket-close",
611
619
  url,
612
- socketId,
620
+ socketId: toSocketId(socketId),
613
621
  timestamp: Date.now(),
614
622
  code: code || 0,
615
- reason: reason || void 0
623
+ reason: reason || void 0,
624
+ source: "builtin"
616
625
  };
617
626
  eventEmitter.emit("websocket-close", event);
618
627
  socketUrlMap.delete(socketId);
@@ -627,10 +636,11 @@ const getWebSocketInspector = () => {
627
636
  const event = {
628
637
  type: "websocket-message-received",
629
638
  url,
630
- socketId,
639
+ socketId: toSocketId(socketId),
631
640
  timestamp: Date.now(),
632
641
  data,
633
- messageType: typeof data === "string" ? "text" : "binary"
642
+ messageType: typeof data === "string" ? "text" : "binary",
643
+ source: "builtin"
634
644
  };
635
645
  eventEmitter.emit("websocket-message-received", event);
636
646
  }
@@ -644,9 +654,10 @@ const getWebSocketInspector = () => {
644
654
  const event = {
645
655
  type: "websocket-error",
646
656
  url,
647
- socketId,
657
+ socketId: toSocketId(socketId),
648
658
  timestamp: Date.now(),
649
- error
659
+ error,
660
+ source: "builtin"
650
661
  };
651
662
  eventEmitter.emit("websocket-error", event);
652
663
  }
@@ -659,10 +670,11 @@ const getWebSocketInspector = () => {
659
670
  const event = {
660
671
  type: "websocket-message-sent",
661
672
  url,
662
- socketId,
673
+ socketId: toSocketId(socketId),
663
674
  timestamp: Date.now(),
664
675
  data,
665
- messageType: typeof data === "string" ? "text" : "binary"
676
+ messageType: typeof data === "string" ? "text" : "binary",
677
+ source: "builtin"
666
678
  };
667
679
  eventEmitter.emit("websocket-message-sent", event);
668
680
  });
@@ -674,8 +686,9 @@ const getWebSocketInspector = () => {
674
686
  const event = {
675
687
  type: "websocket-open",
676
688
  url,
677
- socketId,
678
- timestamp: Date.now()
689
+ socketId: toSocketId(socketId),
690
+ timestamp: Date.now(),
691
+ source: "builtin"
679
692
  };
680
693
  eventEmitter.emit("websocket-open", event);
681
694
  });
@@ -688,10 +701,11 @@ const getWebSocketInspector = () => {
688
701
  const event = {
689
702
  type: "websocket-close",
690
703
  url,
691
- socketId,
704
+ socketId: toSocketId(socketId),
692
705
  timestamp: Date.now(),
693
706
  code: error.code,
694
- reason: error.reason
707
+ reason: error.reason,
708
+ source: "builtin"
695
709
  };
696
710
  eventEmitter.emit("websocket-close", event);
697
711
  socketUrlMap.delete(socketId);
@@ -710,6 +724,9 @@ const getWebSocketInspector = () => {
710
724
  on: (event, callback) => eventEmitter.on(event, callback)
711
725
  };
712
726
  };
727
+ const getWebSocketInspector = () => {
728
+ return createWebSocketInspector();
729
+ };
713
730
  let connectCallback;
714
731
  let messageCallback;
715
732
  let errorCallback;
@@ -843,7 +860,7 @@ const getSSEInspector = () => {
843
860
  status: sseXhr.status,
844
861
  statusText: sseXhr.statusText,
845
862
  headers: sseXhr.responseHeaders || {},
846
- contentType: getContentType(sseXhr),
863
+ contentType: getContentType$1(sseXhr),
847
864
  size: 0,
848
865
  responseTime: Date.now()
849
866
  }
@@ -932,6 +949,7 @@ class EventsListener {
932
949
  constructor() {
933
950
  this.messageQueue = [];
934
951
  this.sendFunction = null;
952
+ this.filterFunction = null;
935
953
  this.maxQueueSize = 200;
936
954
  this.isQueuing = false;
937
955
  }
@@ -949,6 +967,7 @@ class EventsListener {
949
967
  */
950
968
  connect(sendFn, filterFn) {
951
969
  this.sendFunction = sendFn;
970
+ this.filterFunction = filterFn ?? null;
952
971
  this.isQueuing = false;
953
972
  this.flushQueue(filterFn);
954
973
  }
@@ -962,6 +981,9 @@ class EventsListener {
962
981
  if (this.isQueuing) {
963
982
  this.enqueueMessage({ type, data });
964
983
  } else if (this.sendFunction) {
984
+ if (this.filterFunction && !this.filterFunction({ type, data })) {
985
+ return;
986
+ }
965
987
  this.sendFunction(type, data);
966
988
  }
967
989
  }
@@ -989,14 +1011,257 @@ class EventsListener {
989
1011
  const createEventsListener = () => {
990
1012
  return new EventsListener();
991
1013
  };
1014
+ const NITRO_NETWORK_EVENTS = [
1015
+ "request-sent",
1016
+ "response-received",
1017
+ "request-completed",
1018
+ "request-failed",
1019
+ "websocket-connect",
1020
+ "websocket-open",
1021
+ "websocket-close",
1022
+ "websocket-message-sent",
1023
+ "websocket-message-received",
1024
+ "websocket-error"
1025
+ ];
1026
+ const loadNitroModule = () => {
1027
+ try {
1028
+ return require("react-native-nitro-fetch");
1029
+ } catch {
1030
+ return null;
1031
+ }
1032
+ };
1033
+ const timestampOrigin = typeof performance !== "undefined" && typeof performance.timeOrigin === "number" ? performance.timeOrigin : Date.now() - performance.now();
1034
+ const toEpochTime = (timestamp) => Math.round(timestampOrigin + timestamp);
1035
+ const toHeaders = (headers) => {
1036
+ return headers.reduce((acc, { key, value }) => {
1037
+ const existing = acc[key];
1038
+ if (existing === void 0) {
1039
+ acc[key] = value;
1040
+ return acc;
1041
+ }
1042
+ acc[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
1043
+ return acc;
1044
+ }, {});
1045
+ };
1046
+ const toPostData = (body) => {
1047
+ if (body == null) {
1048
+ return void 0;
1049
+ }
1050
+ return {
1051
+ type: "text",
1052
+ value: body
1053
+ };
1054
+ };
1055
+ const cloneEntry = (entry) => {
1056
+ return JSON.parse(JSON.stringify(entry));
1057
+ };
1058
+ const getContentType = (headers) => {
1059
+ return headers.find((header) => header.key.toLowerCase() === "content-type")?.value ?? "text/plain";
1060
+ };
1061
+ const normalizeReadyState = (readyState) => readyState.toUpperCase();
1062
+ const createNitroNetworkInspector = (getNitroModule = loadNitroModule) => {
1063
+ const eventEmitter = createNanoEvents();
1064
+ const previousEntries = /* @__PURE__ */ new Map();
1065
+ const responseBodies = /* @__PURE__ */ new Map();
1066
+ let nitroModule = null;
1067
+ let unsubscribe = null;
1068
+ const emitHttpEvents = (entry, previous) => {
1069
+ if (!previous) {
1070
+ eventEmitter.emit("request-sent", {
1071
+ requestId: entry.id,
1072
+ timestamp: toEpochTime(entry.startTime),
1073
+ request: {
1074
+ url: entry.url,
1075
+ method: entry.method,
1076
+ headers: toHeaders(entry.requestHeaders),
1077
+ postData: toPostData(entry.requestBody)
1078
+ },
1079
+ initiator: { type: "other" },
1080
+ type: "Fetch",
1081
+ source: "nitro"
1082
+ });
1083
+ }
1084
+ if (entry.error) {
1085
+ if (!previous || previous.error !== entry.error) {
1086
+ eventEmitter.emit("request-failed", {
1087
+ requestId: entry.id,
1088
+ timestamp: toEpochTime(entry.endTime || entry.startTime),
1089
+ type: "Fetch",
1090
+ error: entry.error,
1091
+ canceled: entry.error === "Request canceled",
1092
+ source: "nitro"
1093
+ });
1094
+ }
1095
+ return;
1096
+ }
1097
+ const didResponseChange = !previous || previous.status !== entry.status || previous.statusText !== entry.statusText || previous.responseBodySize !== entry.responseBodySize || previous.endTime !== entry.endTime;
1098
+ if (!didResponseChange) {
1099
+ return;
1100
+ }
1101
+ const responseTimestamp = toEpochTime(entry.endTime || entry.startTime);
1102
+ eventEmitter.emit("response-received", {
1103
+ requestId: entry.id,
1104
+ timestamp: responseTimestamp,
1105
+ type: "Fetch",
1106
+ response: {
1107
+ url: entry.url,
1108
+ status: entry.status,
1109
+ statusText: entry.statusText,
1110
+ headers: toHeaders(entry.responseHeaders),
1111
+ contentType: getContentType(entry.responseHeaders),
1112
+ size: entry.responseBodySize,
1113
+ responseTime: responseTimestamp
1114
+ },
1115
+ source: "nitro"
1116
+ });
1117
+ eventEmitter.emit("request-completed", {
1118
+ requestId: entry.id,
1119
+ timestamp: responseTimestamp,
1120
+ duration: entry.duration,
1121
+ size: entry.responseBodySize,
1122
+ ttfb: entry.duration,
1123
+ source: "nitro"
1124
+ });
1125
+ };
1126
+ const emitWebSocketEvents = (entry, previous) => {
1127
+ const socketId = entry.id;
1128
+ const readyState = normalizeReadyState(entry.readyState);
1129
+ const previousReadyState = previous ? normalizeReadyState(previous.readyState) : null;
1130
+ if (!previous) {
1131
+ eventEmitter.emit("websocket-connect", {
1132
+ type: "websocket-connect",
1133
+ url: entry.url,
1134
+ socketId,
1135
+ timestamp: toEpochTime(entry.startTime),
1136
+ protocols: entry.protocols,
1137
+ options: [],
1138
+ source: "nitro"
1139
+ });
1140
+ }
1141
+ if (readyState === "OPEN" && previousReadyState !== "OPEN") {
1142
+ eventEmitter.emit("websocket-open", {
1143
+ type: "websocket-open",
1144
+ url: entry.url,
1145
+ socketId,
1146
+ timestamp: toEpochTime(entry.startTime),
1147
+ source: "nitro"
1148
+ });
1149
+ }
1150
+ const previousMessageCount = previous?.messages.length ?? 0;
1151
+ for (const message of entry.messages.slice(previousMessageCount)) {
1152
+ const event = {
1153
+ url: entry.url,
1154
+ socketId,
1155
+ timestamp: toEpochTime(message.timestamp),
1156
+ data: message.data,
1157
+ messageType: message.isBinary ? "binary" : "text",
1158
+ source: "nitro"
1159
+ };
1160
+ if (message.direction === "sent") {
1161
+ eventEmitter.emit("websocket-message-sent", {
1162
+ type: "websocket-message-sent",
1163
+ ...event
1164
+ });
1165
+ } else {
1166
+ eventEmitter.emit("websocket-message-received", {
1167
+ type: "websocket-message-received",
1168
+ ...event
1169
+ });
1170
+ }
1171
+ }
1172
+ if (entry.error && (!previous || previous.error !== entry.error)) {
1173
+ eventEmitter.emit("websocket-error", {
1174
+ type: "websocket-error",
1175
+ url: entry.url,
1176
+ socketId,
1177
+ timestamp: toEpochTime(entry.endTime || entry.startTime),
1178
+ error: entry.error,
1179
+ source: "nitro"
1180
+ });
1181
+ }
1182
+ if (readyState === "CLOSED" && previousReadyState !== "CLOSED") {
1183
+ eventEmitter.emit("websocket-close", {
1184
+ type: "websocket-close",
1185
+ url: entry.url,
1186
+ socketId,
1187
+ timestamp: toEpochTime(entry.endTime || entry.startTime),
1188
+ code: entry.closeCode ?? 0,
1189
+ reason: entry.closeReason,
1190
+ source: "nitro"
1191
+ });
1192
+ }
1193
+ };
1194
+ const handleEntry = (entry) => {
1195
+ const previous = previousEntries.get(entry.id);
1196
+ if (entry.type === "http") {
1197
+ responseBodies.set(entry.id, entry.responseBody ?? null);
1198
+ emitHttpEvents(entry, previous);
1199
+ } else {
1200
+ emitWebSocketEvents(entry, previous);
1201
+ }
1202
+ previousEntries.set(entry.id, cloneEntry(entry));
1203
+ };
1204
+ return {
1205
+ enable() {
1206
+ if (unsubscribe) {
1207
+ return;
1208
+ }
1209
+ nitroModule = getNitroModule();
1210
+ if (!nitroModule) {
1211
+ return;
1212
+ }
1213
+ nitroModule.NetworkInspector.enable();
1214
+ for (const entry of nitroModule.NetworkInspector.getEntries()) {
1215
+ previousEntries.set(entry.id, cloneEntry(entry));
1216
+ if (entry.type === "http") {
1217
+ responseBodies.set(entry.id, entry.responseBody ?? null);
1218
+ }
1219
+ }
1220
+ unsubscribe = nitroModule.NetworkInspector.onEntry(handleEntry);
1221
+ },
1222
+ disable() {
1223
+ unsubscribe?.();
1224
+ unsubscribe = null;
1225
+ nitroModule?.NetworkInspector.disable();
1226
+ },
1227
+ isEnabled() {
1228
+ return nitroModule?.NetworkInspector.isEnabled() ?? false;
1229
+ },
1230
+ dispose() {
1231
+ unsubscribe?.();
1232
+ unsubscribe = null;
1233
+ previousEntries.clear();
1234
+ responseBodies.clear();
1235
+ nitroModule?.NetworkInspector.disable();
1236
+ nitroModule = null;
1237
+ },
1238
+ getResponseBody(requestId) {
1239
+ return responseBodies.get(requestId) ?? null;
1240
+ },
1241
+ on(event, callback) {
1242
+ return eventEmitter.on(event, callback);
1243
+ }
1244
+ };
1245
+ };
1246
+ const getNitroNetworkInspector = /* @__PURE__ */ (() => {
1247
+ let instance = null;
1248
+ return () => {
1249
+ if (!instance) {
1250
+ instance = createNitroNetworkInspector();
1251
+ }
1252
+ return instance;
1253
+ };
1254
+ })();
992
1255
  const createNetworkInspectorInstance = () => {
993
1256
  const http = getHTTPInspector();
994
1257
  const sse = getSSEInspector();
995
1258
  const websocket = getWebSocketInspector();
1259
+ const nitro = getNitroNetworkInspector();
996
1260
  return {
997
1261
  http,
998
1262
  sse,
999
1263
  websocket,
1264
+ nitro,
1000
1265
  setup(eventsListener) {
1001
1266
  HTTP_EVENTS.forEach((event) => {
1002
1267
  http.on(event, (data) => {
@@ -1013,21 +1278,36 @@ const createNetworkInspectorInstance = () => {
1013
1278
  eventsListener.send(data.type, data);
1014
1279
  });
1015
1280
  });
1281
+ NITRO_NETWORK_EVENTS.forEach((event) => {
1282
+ nitro.on(event, (data) => {
1283
+ eventsListener.send(event, data);
1284
+ });
1285
+ });
1016
1286
  },
1017
1287
  enable(config = { http: true, sse: true, websocket: true }) {
1018
1288
  if (config.http) http.enable();
1019
1289
  if (config.sse) sse.enable();
1020
1290
  if (config.websocket) websocket.enable();
1291
+ if (config.http || config.websocket) nitro.enable();
1021
1292
  },
1022
1293
  disable() {
1023
1294
  http.disable();
1024
1295
  sse.disable();
1025
1296
  websocket.disable();
1297
+ nitro.disable();
1026
1298
  },
1027
1299
  dispose() {
1028
1300
  http.dispose();
1029
1301
  sse.dispose();
1030
1302
  websocket.dispose();
1303
+ nitro.dispose();
1304
+ },
1305
+ async getResponseBody(requestId) {
1306
+ const request = http.getNetworkRequestsRegistry().getEntry(requestId);
1307
+ if (request) {
1308
+ return getResponseBody(request);
1309
+ }
1310
+ return nitro.getResponseBody(requestId);
1031
1311
  }
1032
1312
  };
1033
1313
  };
@@ -1074,11 +1354,10 @@ const withOnBootNetworkActivityRecording = (options) => {
1074
1354
  };
1075
1355
  export {
1076
1356
  DEFAULT_CONFIG as D,
1077
- getOverridesRegistry as a,
1078
- isWebSocketEvent as b,
1357
+ isWebSocketEvent as a,
1358
+ isSSEEvent as b,
1079
1359
  createNetworkInspectorsConfiguration as c,
1080
- isSSEEvent as d,
1081
- getResponseBody as g,
1360
+ getOverridesRegistry as g,
1082
1361
  isHttpEvent as i,
1083
1362
  safeStringify as s,
1084
1363
  validateConfig as v,