@react-native-oh/react-native-harmony 0.72.10 → 0.72.11-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.
@@ -448,4 +448,4 @@ const styles = StyleSheet.create({
448
448
  }),
449
449
  });
450
450
 
451
- module.exports = (Button: ButtonType);
451
+ module.exports = (Button: ButtonType);
@@ -38,7 +38,7 @@ import Dimensions from "react-native/Libraries/Utilities/Dimensions";
38
38
  import dismissKeyboard from "react-native/Libraries/Utilities/dismissKeyboard";
39
39
  import Platform from "../../Utilities/Platform";
40
40
  import Keyboard from "react-native/Libraries/Components/Keyboard/Keyboard";
41
- import TextInputState from "react-native/Libraries/Components/TextInput/TextInputState";
41
+ import TextInputState from "../TextInput/TextInputState.harmony";
42
42
  import View from "react-native/Libraries/Components/View/View";
43
43
  import AndroidHorizontalScrollContentViewNativeComponent from "react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollContentViewNativeComponent";
44
44
  import AndroidHorizontalScrollViewNativeComponent from "react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent";
@@ -914,15 +914,12 @@ class ScrollView extends React.Component<Props, State> {
914
914
  if (this._scrollView.nativeInstance == null) {
915
915
  return;
916
916
  }
917
- // RNOH: patch - This command occurs before aboutToAppear. As a result, scrollTo in ArkTs is not triggered.
918
- setTimeout(() => {
919
- Commands.scrollTo(
920
- this._scrollView.nativeInstance,
921
- x || 0,
922
- y || 0,
923
- animated !== false
924
- );
925
- }, 100)
917
+ Commands.scrollTo(
918
+ this._scrollView.nativeInstance,
919
+ x || 0,
920
+ y || 0,
921
+ animated !== false
922
+ );
926
923
  };
927
924
 
928
925
  /**
@@ -1812,18 +1809,10 @@ class ScrollView extends React.Component<Props, State> {
1812
1809
  // default to true
1813
1810
  snapToEnd: this.props.snapToEnd !== false,
1814
1811
  // pagingEnabled is overridden by snapToInterval / snapToOffsets
1815
- pagingEnabled: Platform.select({
1816
- // on iOS, pagingEnabled must be set to false to have snapToInterval / snapToOffsets work
1817
- ios:
1812
+ pagingEnabled:
1818
1813
  this.props.pagingEnabled === true &&
1819
1814
  this.props.snapToInterval == null &&
1820
- this.props.snapToOffsets == null,
1821
- // on Android, pagingEnabled must be set to true to have snapToInterval / snapToOffsets work
1822
- android:
1823
- this.props.pagingEnabled === true ||
1824
- this.props.snapToInterval != null ||
1825
- this.props.snapToOffsets != null,
1826
- }),
1815
+ this.props.snapToOffsets == null,
1827
1816
  };
1828
1817
 
1829
1818
  const { decelerationRate } = this.props;
@@ -30,7 +30,7 @@ import Text from 'react-native/Libraries/Text/Text';
30
30
  import TextAncestor from 'react-native/Libraries/Text/TextAncestor';
31
31
  import Platform from '../../Utilities/Platform';
32
32
  import useMergeRefs from 'react-native/Libraries/Utilities/useMergeRefs';
33
- import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState';
33
+ import TextInputState from './TextInputState.harmony';
34
34
  import invariant from 'invariant';
35
35
  import nullthrows from 'nullthrows';
36
36
  import * as React from 'react';
package/index.js CHANGED
@@ -65,7 +65,7 @@ module.exports = {
65
65
  get Keyboard() {
66
66
  return require('react-native/Libraries/Components/Keyboard/Keyboard');
67
67
  },
68
- get KeyboardAvoidingView(){
68
+ get KeyboardAvoidingView() {
69
69
  return require('react-native/Libraries/Components/Keyboard/KeyboardAvoidingView')
70
70
  .default;
71
71
  },
@@ -89,6 +89,13 @@ module.exports = {
89
89
  get RefreshControl() {
90
90
  return require('./Libraries/Components/RefreshControl/RefreshControl');
91
91
  },
92
+ get requireNativeComponent() {
93
+ return require('react-native/Libraries/ReactNative/requireNativeComponent')
94
+ .default;
95
+ },
96
+ get RootTagContext() {
97
+ return require('react-native/Libraries/ReactNative/RootTag').RootTagContext;
98
+ },
92
99
  get SafeAreaView() {
93
100
  return require('./Libraries/Components/SafeAreaView/SafeAreaView').default;
94
101
  },
@@ -114,16 +121,16 @@ module.exports = {
114
121
  return require('react-native/Libraries/Components/Touchable/Touchable');
115
122
  },
116
123
  get TouchableHighlight() {
117
- return require("./Libraries/Components/Touchable/TouchableHighlight");
124
+ return require('./Libraries/Components/Touchable/TouchableHighlight');
118
125
  },
119
126
  get TouchableNativeFeedback() {
120
- return require("./Libraries/Components/Touchable/TouchableNativeFeedback");
127
+ return require('./Libraries/Components/Touchable/TouchableNativeFeedback');
121
128
  },
122
129
  get TouchableOpacity() {
123
130
  return require('react-native/Libraries/Components/Touchable/TouchableOpacity');
124
131
  },
125
132
  get TouchableWithoutFeedback() {
126
- return require("./Libraries/Components/Touchable/TouchableWithoutFeedback");
133
+ return require('./Libraries/Components/Touchable/TouchableWithoutFeedback');
127
134
  },
128
135
  get TurboModuleRegistry() {
129
136
  return require('react-native/Libraries/TurboModule/TurboModuleRegistry');
@@ -174,5 +181,8 @@ module.exports = {
174
181
  return require('react-native/Libraries/Renderer/shims/ReactNative')
175
182
  .dispatchCommand;
176
183
  },
184
+ get EventEmitter() {
185
+ return require('react-native/Libraries/vendor/emitter/EventEmitter')
186
+ }
177
187
  // END: react-native-harmony specific exports
178
188
  };
package/metro.config.js CHANGED
@@ -1,79 +1,109 @@
1
1
  //@ts-check
2
2
  const pathUtils = require('path');
3
3
  const fs = require('fs');
4
+ const colors = require('colors/safe');
5
+
6
+ let shouldPrintInfoAboutRNRedirection = true;
4
7
 
5
8
  /**
6
- * @type {import("metro-config").ConfigT}
9
+ * @param msg {string}
7
10
  */
8
- module.exports = {
9
- transformer: {
10
- assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry',
11
- getTransformOptions: async () => ({
12
- transform: {
13
- experimentalImportSupport: false,
14
- inlineRequires: true,
15
- },
16
- }),
17
- },
18
- resolver: {
19
- /** By default, Metro pickups files from native, "harmony" directory what causes conflicts. */
20
- blockList: [/\\harmony\/.*/],
21
- resolveRequest: (ctx, moduleName, platform) => {
22
- if (platform === 'harmony') {
23
- if (moduleName === 'react-native') {
24
- return ctx.resolveRequest(ctx, 'react-native-harmony', platform);
25
- } else if (moduleName.startsWith('react-native/')) {
26
- return ctx.resolveRequest(ctx, moduleName, 'ios');
27
- } else if (isInternalReactNativeRelativeImport(ctx.originModulePath)) {
28
- if (moduleName.startsWith('.')) {
29
- const moduleAbsPath = pathUtils.resolve(
30
- pathUtils.dirname(ctx.originModulePath),
31
- moduleName
32
- );
33
- const [_, modulePathRelativeToReactNative] = moduleAbsPath.split(
34
- `${pathUtils.sep}node_modules${pathUtils.sep}react-native${pathUtils.sep}`
11
+ function info(msg) {
12
+ const infoPrefix = '[' + colors.bold(colors.cyan(`INFO`)) + ']';
13
+ console.log(infoPrefix, msg);
14
+ }
15
+
16
+ /**
17
+ * @param options {{reactNativeHarmonyPackageName: string} | undefined}
18
+ * @returns {import("metro-config").InputConfigT}
19
+ */
20
+ function createHarmonyMetroConfig(options) {
21
+ const reactNativeHarmonyName =
22
+ options?.reactNativeHarmonyPackageName ?? 'react-native-harmony';
23
+ return {
24
+ transformer: {
25
+ assetRegistryPath: 'react-native/Libraries/Image/AssetRegistry',
26
+ getTransformOptions: async () => ({
27
+ transform: {
28
+ experimentalImportSupport: false,
29
+ inlineRequires: true,
30
+ },
31
+ }),
32
+ },
33
+ resolver: {
34
+ /** By default, Metro pickups files from native, "harmony" directory what causes conflicts. */
35
+ blockList: [/\\harmony\/.*/],
36
+ resolveRequest: (ctx, moduleName, platform) => {
37
+ if (platform === 'harmony') {
38
+ if (shouldPrintInfoAboutRNRedirection) {
39
+ info(
40
+ `Redirected imports from ${colors.bold(
41
+ colors.gray('react-native'),
42
+ )} to ${colors.bold(reactNativeHarmonyName)}`,
35
43
  );
36
- try {
37
- return ctx.resolveRequest(
38
- ctx,
39
- `react-native-harmony${pathUtils.sep}${modulePathRelativeToReactNative}`,
40
- 'harmony'
41
- );
42
- } catch (err) {}
44
+ shouldPrintInfoAboutRNRedirection = false;
43
45
  }
44
- return ctx.resolveRequest(ctx, moduleName, 'ios');
45
- } else {
46
- /**
47
- * Replace `react-native-foo` with `react-native-harmony-foo` if a package has harmony directory and proper package.json configuration e.g.
48
- *
49
- * react-native-harmony-foo/package.json:
50
- * "harmony": {
51
- * "alias": "react-native-foo"
52
- * }
53
- */
54
- const harmonyPackageNameByAlias = getHarmonyPackageNameByAlias('.');
55
- const alias = getPackageName(moduleName);
56
- if (alias) {
57
- const harmonyPackageName = harmonyPackageNameByAlias[alias];
58
- if (
59
- harmonyPackageName &&
60
- !isRequestFromHarmonyPackage(
61
- ctx.originModulePath,
62
- harmonyPackageName
63
- )
64
- ) {
65
- return ctx.resolveRequest(
66
- ctx,
67
- moduleName.replace(alias, harmonyPackageName),
68
- platform
46
+ if (moduleName === 'react-native') {
47
+ return ctx.resolveRequest(ctx, reactNativeHarmonyName, platform);
48
+ } else if (moduleName.startsWith('react-native/')) {
49
+ return ctx.resolveRequest(ctx, moduleName, 'ios');
50
+ } else if (
51
+ isInternalReactNativeRelativeImport(ctx.originModulePath)
52
+ ) {
53
+ if (moduleName.startsWith('.')) {
54
+ const moduleAbsPath = pathUtils.resolve(
55
+ pathUtils.dirname(ctx.originModulePath),
56
+ moduleName,
69
57
  );
58
+ const [_, modulePathRelativeToReactNative] = moduleAbsPath.split(
59
+ `${pathUtils.sep}node_modules${pathUtils.sep}react-native${pathUtils.sep}`,
60
+ );
61
+ try {
62
+ return ctx.resolveRequest(
63
+ ctx,
64
+ `${reactNativeHarmonyName}${pathUtils.sep}${modulePathRelativeToReactNative}`,
65
+ 'harmony',
66
+ );
67
+ } catch (err) {}
68
+ }
69
+ return ctx.resolveRequest(ctx, moduleName, 'ios');
70
+ } else {
71
+ /**
72
+ * Replace `react-native-foo` with `react-native-harmony-foo` if a package has harmony directory and proper package.json configuration e.g.
73
+ *
74
+ * react-native-harmony-foo/package.json:
75
+ * "harmony": {
76
+ * "alias": "react-native-foo"
77
+ * }
78
+ */
79
+ const harmonyPackageNameByAlias = getHarmonyPackageNameByAlias('.');
80
+ const alias = getPackageName(moduleName);
81
+ if (alias) {
82
+ const harmonyPackageName = harmonyPackageNameByAlias[alias];
83
+ if (
84
+ harmonyPackageName &&
85
+ !isRequestFromHarmonyPackage(
86
+ ctx.originModulePath,
87
+ harmonyPackageName,
88
+ )
89
+ ) {
90
+ return ctx.resolveRequest(
91
+ ctx,
92
+ moduleName.replace(alias, harmonyPackageName),
93
+ platform,
94
+ );
95
+ }
70
96
  }
71
97
  }
72
98
  }
73
- }
74
- return ctx.resolveRequest(ctx, moduleName, platform);
99
+ return ctx.resolveRequest(ctx, moduleName, platform);
100
+ },
75
101
  },
76
- },
102
+ };
103
+ }
104
+
105
+ module.exports = {
106
+ createHarmonyMetroConfig,
77
107
  };
78
108
 
79
109
  /**
@@ -104,7 +134,7 @@ function getPackageName(moduleName) {
104
134
  */
105
135
  function isInternalReactNativeRelativeImport(originModulePath) {
106
136
  return originModulePath.includes(
107
- `${pathUtils.sep}node_modules${pathUtils.sep}react-native${pathUtils.sep}`
137
+ `${pathUtils.sep}node_modules${pathUtils.sep}react-native${pathUtils.sep}`,
108
138
  );
109
139
  }
110
140
 
@@ -114,8 +144,10 @@ function isInternalReactNativeRelativeImport(originModulePath) {
114
144
  * @returns {boolean}
115
145
  */
116
146
  function isRequestFromHarmonyPackage(originModulePath, harmonyPackageName) {
147
+ const slashes = new RegExp('/', 'g');
148
+ const packagePath = harmonyPackageName.replace(slashes, pathUtils.sep);
117
149
  return originModulePath.includes(
118
- `${pathUtils.sep}node_modules${pathUtils.sep}${harmonyPackageName}${pathUtils.sep}`
150
+ `${pathUtils.sep}node_modules${pathUtils.sep}${packagePath}${pathUtils.sep}`,
119
151
  );
120
152
  }
121
153
 
@@ -136,13 +168,20 @@ function getHarmonyPackageNameByAlias(projectRootPath) {
136
168
  return cachedHarmonyPackageAliasByName;
137
169
  }
138
170
  cachedHarmonyPackageAliasByName = findHarmonyNodeModulePaths(
139
- projectRootPath
171
+ findHarmonyNodeModuleSearchPaths(projectRootPath),
140
172
  ).reduce((acc, harmonyNodeModulePath) => {
141
173
  const harmonyNodeModulePathSegments = harmonyNodeModulePath.split(
142
- pathUtils.sep
174
+ pathUtils.sep,
143
175
  );
144
- const harmonyNodeModuleName =
176
+ let harmonyNodeModuleName =
145
177
  harmonyNodeModulePathSegments[harmonyNodeModulePathSegments.length - 1];
178
+ if (harmonyNodeModulePathSegments.length > 1) {
179
+ const harmonyNodeModuleParentDirName =
180
+ harmonyNodeModulePathSegments[harmonyNodeModulePathSegments.length - 2];
181
+ if (harmonyNodeModuleParentDirName.startsWith('@')) {
182
+ harmonyNodeModuleName = `${harmonyNodeModuleParentDirName}/${harmonyNodeModuleName}`;
183
+ }
184
+ }
146
185
  const packageJSONPath = `${harmonyNodeModulePath}${pathUtils.sep}package.json`;
147
186
  const packageJSON = readHarmonyModulePackageJSON(packageJSONPath);
148
187
  const alias = packageJSON.harmony?.alias;
@@ -151,8 +190,32 @@ function getHarmonyPackageNameByAlias(projectRootPath) {
151
190
  }
152
191
  return acc;
153
192
  }, initialAcc);
154
- if (Object.keys(cachedHarmonyPackageAliasByName).length > 0)
155
- console.log(cachedHarmonyPackageAliasByName);
193
+ const harmonyPackagesCount = Object.keys(
194
+ cachedHarmonyPackageAliasByName,
195
+ ).length;
196
+ if (harmonyPackagesCount > 0) {
197
+ const prettyHarmonyPackagesCount = colors.bold(
198
+ harmonyPackagesCount > 0
199
+ ? colors.green(harmonyPackagesCount.toString())
200
+ : harmonyPackagesCount.toString(),
201
+ );
202
+ info(
203
+ `Redirected imports to ${prettyHarmonyPackagesCount} harmony-specific third-party package(s):`,
204
+ );
205
+ if (harmonyPackagesCount > 0) {
206
+ Object.entries(cachedHarmonyPackageAliasByName).forEach(
207
+ ([original, alias]) => {
208
+ info(
209
+ `• ${colors.bold(colors.gray(original))} → ${colors.bold(alias)}`,
210
+ );
211
+ },
212
+ );
213
+ }
214
+ } else {
215
+ info('No harmony-specific third-party packages have been detected');
216
+ }
217
+ console.log('');
218
+
156
219
  return cachedHarmonyPackageAliasByName;
157
220
  }
158
221
 
@@ -160,12 +223,29 @@ function getHarmonyPackageNameByAlias(projectRootPath) {
160
223
  * @param projectRootPath {string}
161
224
  * @returns {string[]}
162
225
  */
163
- function findHarmonyNodeModulePaths(projectRootPath) {
226
+ function findHarmonyNodeModuleSearchPaths(projectRootPath) {
164
227
  const nodeModulesPath = `${projectRootPath}${pathUtils.sep}node_modules`;
165
- return fs
228
+ const searchPaths = fs
166
229
  .readdirSync(nodeModulesPath)
167
- .map((dirName) => `${nodeModulesPath}${pathUtils.sep}${dirName}`)
168
- .filter(hasNodeModulePathHarmonyCode);
230
+ .filter(dirName => dirName.startsWith('@'))
231
+ .map(dirName => `${nodeModulesPath}${pathUtils.sep}${dirName}`);
232
+ searchPaths.push(nodeModulesPath);
233
+ return searchPaths;
234
+ }
235
+
236
+ /**
237
+ * @param searchPaths {string[]}
238
+ * @returns {string[]}
239
+ */
240
+ function findHarmonyNodeModulePaths(searchPaths) {
241
+ return searchPaths
242
+ .map(searchPath => {
243
+ return fs
244
+ .readdirSync(searchPath)
245
+ .map(dirName => `${searchPath}${pathUtils.sep}${dirName}`)
246
+ .filter(hasNodeModulePathHarmonyCode);
247
+ })
248
+ .flat();
169
249
  }
170
250
 
171
251
  /**
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@react-native-oh/react-native-harmony",
3
- "version": "0.72.10",
4
- "publishConfig": {
3
+ "version": "0.72.11-2",
4
+ "description": "",
5
+ "publishConfig": {
5
6
  "registry": "https://registry.npmjs.org/",
6
7
  "access": "public"
7
8
  },
8
- "description": "",
9
9
  "scripts": {
10
10
  "install:dev-cli": "cd ../react-native-harmony-cli && npm pack && cd ../react-native-harmony && npm i ../react-native-harmony-cli/rnoh-react-native-harmony-cli-0.0.15.tgz && cd ../react-native-harmony",
11
11
  "pack:harmony": "react-native pack-harmony --oh-module-path ../tester/harmony/rnoh --harmony-dir-path ./harmony --package-json-path ./package.json",
@@ -38,6 +38,7 @@
38
38
  ],
39
39
  "dependencies": {
40
40
  "@react-native-oh/react-native-harmony-cli": "^0.0.15",
41
+ "colors": "^1.4.0",
41
42
  "fs-extra": "^11.1.1",
42
43
  "metro": "^0.76.3",
43
44
  "metro-config": "^0.76.3"
package/types/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // export * from 'react-native/Libraries/ActionSheetIOS/ActionSheetIOS';
2
- export * from "react-native/Libraries/Alert/Alert";
2
+ export * from 'react-native/Libraries/Alert/Alert';
3
3
  export * from 'react-native/Libraries/Animated/Animated';
4
4
  export * from 'react-native/Libraries/Animated/Easing';
5
5
  export * from 'react-native/Libraries/Animated/useAnimatedValue';
@@ -31,7 +31,7 @@ export * from 'react-native/Libraries/Components/Touchable/TouchableNativeFeedba
31
31
  export * from 'react-native/Libraries/Components/Touchable/TouchableOpacity';
32
32
  export * from 'react-native/Libraries/Components/Touchable/TouchableWithoutFeedback';
33
33
  export * from 'react-native/Libraries/Components/View/View';
34
- // export * from 'react-native/Libraries/Components/View/ViewAccessibility';
34
+ export * from 'react-native/Libraries/Components/View/ViewAccessibility';
35
35
  export * from 'react-native/Libraries/Components/View/ViewPropTypes';
36
36
  export * from 'react-native/Libraries/Components/Button';
37
37
  export * from 'react-native/Libraries/EventEmitter/NativeEventEmitter';
@@ -47,7 +47,7 @@ export * from 'react-native/Libraries/LayoutAnimation/LayoutAnimation';
47
47
  export * from 'react-native/Libraries/Linking/Linking';
48
48
  export * from 'react-native/Libraries/Lists/FlatList';
49
49
  export * from 'react-native/Libraries/Lists/SectionList';
50
- export * from 'react-native/Libraries/Lists/VirtualizedList';
50
+ export * from '@react-native/virtualized-lists';
51
51
  // export * from 'react-native/Libraries/LogBox/LogBox';
52
52
  export * from 'react-native/Libraries/Modal/Modal';
53
53
  // export * as Systrace from 'react-native/Libraries/Performance/Systrace';
@@ -56,9 +56,9 @@ export * from 'react-native/Libraries/Modal/Modal';
56
56
  export * from 'react-native/Libraries/ReactNative/AppRegistry';
57
57
  export * from 'react-native/Libraries/ReactNative/I18nManager';
58
58
  export * from 'react-native/Libraries/ReactNative/RendererProxy';
59
- // export * from 'react-native/Libraries/ReactNative/RootTag';
59
+ export * from 'react-native/Libraries/ReactNative/RootTag';
60
60
  export * from 'react-native/Libraries/ReactNative/UIManager';
61
- // export * from 'react-native/Libraries/ReactNative/requireNativeComponent';
61
+ export * from 'react-native/Libraries/ReactNative/requireNativeComponent';
62
62
  // export * from 'react-native/Libraries/Settings/Settings';
63
63
  // export * from 'react-native/Libraries/Share/Share';
64
64
  // export * from 'react-native/Libraries/StyleSheet/PlatformColorValueTypesIOS';
@@ -69,7 +69,7 @@ export * from 'react-native/Libraries/StyleSheet/processColor';
69
69
  export * from 'react-native/Libraries/Text/Text';
70
70
  // export * from 'react-native/Libraries/TurboModule/RCTExport';
71
71
  export * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
72
- // export * from 'react-native/Libraries/Types/CoreEventTypes';
72
+ export * from 'react-native/Libraries/Types/CoreEventTypes';
73
73
  export * from 'react-native/Libraries/Utilities/Appearance';
74
74
  export * from 'react-native/Libraries/Utilities/BackHandler';
75
75
  // export * from 'react-native/Libraries/Utilities/DevSettings';
@@ -79,7 +79,7 @@ export * from '../Libraries/Utilities/Platform';
79
79
  export * from 'react-native/Libraries/Vibration/Vibration';
80
80
  // export * from 'react-native/Libraries/YellowBox/YellowBoxDeprecated';
81
81
  // export * from 'react-native/Libraries/vendor/core/ErrorUtils';
82
- // export * from 'react-native/Libraries/vendor/emitter/EventEmitter';
82
+ export * from 'react-native/Libraries/vendor/emitter/EventEmitter';
83
83
 
84
84
  export * from 'react-native/types/public/DeprecatedPropertiesAlias';
85
85
  export * from 'react-native/types/public/Insets';
@@ -1,677 +0,0 @@
1
- /**
2
- * RNOH: patch
3
- * - imports
4
- * - forces responseType to be 'text'
5
- */
6
-
7
- /**
8
- * Copyright (c) Meta Platforms, Inc. and affiliates.
9
- *
10
- * This source code is licensed under the MIT license found in the
11
- * LICENSE file in the root directory of this source tree.
12
- *
13
- * @format
14
- * @flow
15
- */
16
-
17
- 'use strict';
18
-
19
- import type {IPerformanceLogger} from 'react-native/Libraries/Utilities/createPerformanceLogger';
20
-
21
- import {type EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter';
22
- import EventTarget from 'event-target-shim';
23
-
24
- const BlobManager = require('react-native/Libraries/Blob/BlobManager');
25
- const GlobalPerformanceLogger = require('react-native/Libraries/Utilities/GlobalPerformanceLogger');
26
- const RCTNetworking = require('react-native/Libraries/Network/RCTNetworking').default;
27
- const base64 = require('base64-js');
28
- const invariant = require('invariant');
29
-
30
- const DEBUG_NETWORK_SEND_DELAY: false = false; // Set to a number of milliseconds when debugging
31
-
32
- export type NativeResponseType = 'base64' | 'blob' | 'text';
33
- export type ResponseType =
34
- | ''
35
- | 'arraybuffer'
36
- | 'blob'
37
- | 'document'
38
- | 'json'
39
- | 'text';
40
- export type Response = ?Object | string;
41
-
42
- type XHRInterceptor = interface {
43
- requestSent(id: number, url: string, method: string, headers: Object): void,
44
- responseReceived(
45
- id: number,
46
- url: string,
47
- status: number,
48
- headers: Object,
49
- ): void,
50
- dataReceived(id: number, data: string): void,
51
- loadingFinished(id: number, encodedDataLength: number): void,
52
- loadingFailed(id: number, error: string): void,
53
- };
54
-
55
- // The native blob module is optional so inject it here if available.
56
- if (BlobManager.isAvailable) {
57
- BlobManager.addNetworkingHandler();
58
- }
59
-
60
- const UNSENT = 0;
61
- const OPENED = 1;
62
- const HEADERS_RECEIVED = 2;
63
- const LOADING = 3;
64
- const DONE = 4;
65
-
66
- const SUPPORTED_RESPONSE_TYPES = {
67
- arraybuffer: false, // RNOH: patch - typeof global.ArrayBuffer === 'function',
68
- blob: false, // RNOH: patch - typeof global.Blob === 'function',
69
- document: false,
70
- json: true,
71
- text: true,
72
- '': true,
73
- };
74
-
75
- const REQUEST_EVENTS = [
76
- 'abort',
77
- 'error',
78
- 'load',
79
- 'loadstart',
80
- 'progress',
81
- 'timeout',
82
- 'loadend',
83
- ];
84
-
85
- const XHR_EVENTS = REQUEST_EVENTS.concat('readystatechange');
86
-
87
- class XMLHttpRequestEventTarget extends (EventTarget(...REQUEST_EVENTS): any) {
88
- onload: ?Function;
89
- onloadstart: ?Function;
90
- onprogress: ?Function;
91
- ontimeout: ?Function;
92
- onerror: ?Function;
93
- onabort: ?Function;
94
- onloadend: ?Function;
95
- }
96
-
97
- /**
98
- * Shared base for platform-specific XMLHttpRequest implementations.
99
- */
100
- class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): any) {
101
- static UNSENT: number = UNSENT;
102
- static OPENED: number = OPENED;
103
- static HEADERS_RECEIVED: number = HEADERS_RECEIVED;
104
- static LOADING: number = LOADING;
105
- static DONE: number = DONE;
106
-
107
- static _interceptor: ?XHRInterceptor = null;
108
-
109
- UNSENT: number = UNSENT;
110
- OPENED: number = OPENED;
111
- HEADERS_RECEIVED: number = HEADERS_RECEIVED;
112
- LOADING: number = LOADING;
113
- DONE: number = DONE;
114
-
115
- // EventTarget automatically initializes these to `null`.
116
- onload: ?Function;
117
- onloadstart: ?Function;
118
- onprogress: ?Function;
119
- ontimeout: ?Function;
120
- onerror: ?Function;
121
- onabort: ?Function;
122
- onloadend: ?Function;
123
- onreadystatechange: ?Function;
124
-
125
- readyState: number = UNSENT;
126
- responseHeaders: ?Object;
127
- status: number = 0;
128
- timeout: number = 0;
129
- responseURL: ?string;
130
- withCredentials: boolean = true;
131
-
132
- upload: XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
133
-
134
- _requestId: ?number;
135
- _subscriptions: Array<EventSubscription>;
136
-
137
- _aborted: boolean = false;
138
- _cachedResponse: Response;
139
- _hasError: boolean = false;
140
- _headers: Object;
141
- _lowerCaseResponseHeaders: Object;
142
- _method: ?string = null;
143
- _perfKey: ?string = null;
144
- _responseType: ResponseType;
145
- _response: string = '';
146
- _sent: boolean;
147
- _url: ?string = null;
148
- _timedOut: boolean = false;
149
- _trackingName: string = 'unknown';
150
- _incrementalEvents: boolean = false;
151
- _performanceLogger: IPerformanceLogger = GlobalPerformanceLogger;
152
-
153
- static setInterceptor(interceptor: ?XHRInterceptor) {
154
- XMLHttpRequest._interceptor = interceptor;
155
- }
156
-
157
- constructor() {
158
- super();
159
- this._reset();
160
- }
161
-
162
- _reset(): void {
163
- this.readyState = this.UNSENT;
164
- this.responseHeaders = undefined;
165
- this.status = 0;
166
- delete this.responseURL;
167
-
168
- this._requestId = null;
169
-
170
- this._cachedResponse = undefined;
171
- this._hasError = false;
172
- this._headers = {};
173
- this._response = '';
174
- this._responseType = '';
175
- this._sent = false;
176
- this._lowerCaseResponseHeaders = {};
177
-
178
- this._clearSubscriptions();
179
- this._timedOut = false;
180
- }
181
-
182
- get responseType(): ResponseType {
183
- return this._responseType;
184
- }
185
-
186
- set responseType(responseType: ResponseType): void {
187
- if (this._sent) {
188
- throw new Error(
189
- "Failed to set the 'responseType' property on 'XMLHttpRequest': The " +
190
- 'response type cannot be set after the request has been sent.',
191
- );
192
- }
193
-
194
- // RNOH: patch
195
- this._responseType = "text"
196
- return
197
- if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) {
198
- console.warn(
199
- `The provided value '${responseType}' is not a valid 'responseType'.`,
200
- );
201
- return;
202
- }
203
-
204
- // redboxes early, e.g. for 'arraybuffer' on ios 7
205
- invariant(
206
- SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
207
- `The provided value '${responseType}' is unsupported in this environment.`,
208
- );
209
-
210
- if (responseType === 'blob') {
211
- invariant(
212
- BlobManager.isAvailable,
213
- 'Native module BlobModule is required for blob support',
214
- );
215
- }
216
- this._responseType = responseType;
217
- }
218
-
219
- get responseText(): string {
220
- if (this._responseType !== '' && this._responseType !== 'text') {
221
- throw new Error(
222
- "The 'responseText' property is only available if 'responseType' " +
223
- `is set to '' or 'text', but it is '${this._responseType}'.`,
224
- );
225
- }
226
- if (this.readyState < LOADING) {
227
- return '';
228
- }
229
- return this._response;
230
- }
231
-
232
- get response(): Response {
233
- const {responseType} = this;
234
- if (responseType === '' || responseType === 'text') {
235
- return this.readyState < LOADING || this._hasError ? '' : this._response;
236
- }
237
-
238
- if (this.readyState !== DONE) {
239
- return null;
240
- }
241
-
242
- if (this._cachedResponse !== undefined) {
243
- return this._cachedResponse;
244
- }
245
-
246
- switch (responseType) {
247
- case 'document':
248
- this._cachedResponse = null;
249
- break;
250
-
251
- case 'arraybuffer':
252
- this._cachedResponse = base64.toByteArray(this._response).buffer;
253
- break;
254
-
255
- case 'blob':
256
- if (typeof this._response === 'object' && this._response) {
257
- this._cachedResponse = BlobManager.createFromOptions(this._response);
258
- } else if (this._response === '') {
259
- this._cachedResponse = BlobManager.createFromParts([]);
260
- } else {
261
- throw new Error(`Invalid response for blob: ${this._response}`);
262
- }
263
- break;
264
-
265
- case 'json':
266
- try {
267
- this._cachedResponse = JSON.parse(this._response);
268
- } catch (_) {
269
- this._cachedResponse = null;
270
- }
271
- break;
272
-
273
- default:
274
- this._cachedResponse = null;
275
- }
276
-
277
- return this._cachedResponse;
278
- }
279
-
280
- // exposed for testing
281
- __didCreateRequest(requestId: number): void {
282
- this._requestId = requestId;
283
-
284
- XMLHttpRequest._interceptor &&
285
- XMLHttpRequest._interceptor.requestSent(
286
- requestId,
287
- this._url || '',
288
- this._method || 'GET',
289
- this._headers,
290
- );
291
- }
292
-
293
- // exposed for testing
294
- __didUploadProgress(
295
- requestId: number,
296
- progress: number,
297
- total: number,
298
- ): void {
299
- if (requestId === this._requestId) {
300
- this.upload.dispatchEvent({
301
- type: 'progress',
302
- lengthComputable: true,
303
- loaded: progress,
304
- total,
305
- });
306
- }
307
- }
308
-
309
- __didReceiveResponse(
310
- requestId: number,
311
- status: number,
312
- responseHeaders: ?Object,
313
- responseURL: ?string,
314
- ): void {
315
- if (requestId === this._requestId) {
316
- this._perfKey != null &&
317
- this._performanceLogger.stopTimespan(this._perfKey);
318
- this.status = status;
319
- this.setResponseHeaders(responseHeaders);
320
- this.setReadyState(this.HEADERS_RECEIVED);
321
- if (responseURL || responseURL === '') {
322
- this.responseURL = responseURL;
323
- } else {
324
- delete this.responseURL;
325
- }
326
-
327
- XMLHttpRequest._interceptor &&
328
- XMLHttpRequest._interceptor.responseReceived(
329
- requestId,
330
- responseURL || this._url || '',
331
- status,
332
- responseHeaders || {},
333
- );
334
- }
335
- }
336
-
337
- __didReceiveData(requestId: number, response: string): void {
338
- if (requestId !== this._requestId) {
339
- return;
340
- }
341
- this._response = response;
342
- this._cachedResponse = undefined; // force lazy recomputation
343
- this.setReadyState(this.LOADING);
344
-
345
- XMLHttpRequest._interceptor &&
346
- XMLHttpRequest._interceptor.dataReceived(requestId, response);
347
- }
348
-
349
- __didReceiveIncrementalData(
350
- requestId: number,
351
- responseText: string,
352
- progress: number,
353
- total: number,
354
- ) {
355
- if (requestId !== this._requestId) {
356
- return;
357
- }
358
- if (!this._response) {
359
- this._response = responseText;
360
- } else {
361
- this._response += responseText;
362
- }
363
-
364
- XMLHttpRequest._interceptor &&
365
- XMLHttpRequest._interceptor.dataReceived(requestId, responseText);
366
-
367
- this.setReadyState(this.LOADING);
368
- this.__didReceiveDataProgress(requestId, progress, total);
369
- }
370
-
371
- __didReceiveDataProgress(
372
- requestId: number,
373
- loaded: number,
374
- total: number,
375
- ): void {
376
- if (requestId !== this._requestId) {
377
- return;
378
- }
379
- this.dispatchEvent({
380
- type: 'progress',
381
- lengthComputable: total >= 0,
382
- loaded,
383
- total,
384
- });
385
- }
386
-
387
- // exposed for testing
388
- __didCompleteResponse(
389
- requestId: number,
390
- error: string,
391
- timeOutError: boolean,
392
- ): void {
393
- if (requestId === this._requestId) {
394
- if (error) {
395
- if (this._responseType === '' || this._responseType === 'text') {
396
- this._response = error;
397
- }
398
- this._hasError = true;
399
- if (timeOutError) {
400
- this._timedOut = true;
401
- }
402
- }
403
- this._clearSubscriptions();
404
- this._requestId = null;
405
- this.setReadyState(this.DONE);
406
-
407
- if (error) {
408
- XMLHttpRequest._interceptor &&
409
- XMLHttpRequest._interceptor.loadingFailed(requestId, error);
410
- } else {
411
- XMLHttpRequest._interceptor &&
412
- XMLHttpRequest._interceptor.loadingFinished(
413
- requestId,
414
- this._response.length,
415
- );
416
- }
417
- }
418
- }
419
-
420
- _clearSubscriptions(): void {
421
- (this._subscriptions || []).forEach(sub => {
422
- if (sub) {
423
- sub.remove();
424
- }
425
- });
426
- this._subscriptions = [];
427
- }
428
-
429
- getAllResponseHeaders(): ?string {
430
- if (!this.responseHeaders) {
431
- // according to the spec, return null if no response has been received
432
- return null;
433
- }
434
-
435
- // Assign to non-nullable local variable.
436
- const responseHeaders = this.responseHeaders;
437
-
438
- const unsortedHeaders: Map<
439
- string,
440
- {lowerHeaderName: string, upperHeaderName: string, headerValue: string},
441
- > = new Map();
442
- for (const rawHeaderName of Object.keys(responseHeaders)) {
443
- const headerValue = responseHeaders[rawHeaderName];
444
- const lowerHeaderName = rawHeaderName.toLowerCase();
445
- const header = unsortedHeaders.get(lowerHeaderName);
446
- if (header) {
447
- header.headerValue += ', ' + headerValue;
448
- unsortedHeaders.set(lowerHeaderName, header);
449
- } else {
450
- unsortedHeaders.set(lowerHeaderName, {
451
- lowerHeaderName,
452
- upperHeaderName: rawHeaderName.toUpperCase(),
453
- headerValue,
454
- });
455
- }
456
- }
457
-
458
- // Sort in ascending order, with a being less than b if a's name is legacy-uppercased-byte less than b's name.
459
- const sortedHeaders = [...unsortedHeaders.values()].sort((a, b) => {
460
- if (a.upperHeaderName < b.upperHeaderName) {
461
- return -1;
462
- }
463
- if (a.upperHeaderName > b.upperHeaderName) {
464
- return 1;
465
- }
466
- return 0;
467
- });
468
-
469
- // Combine into single text response.
470
- return (
471
- sortedHeaders
472
- .map(header => {
473
- return header.lowerHeaderName + ': ' + header.headerValue;
474
- })
475
- .join('\r\n') + '\r\n'
476
- );
477
- }
478
-
479
- getResponseHeader(header: string): ?string {
480
- const value = this._lowerCaseResponseHeaders[header.toLowerCase()];
481
- return value !== undefined ? value : null;
482
- }
483
-
484
- setRequestHeader(header: string, value: any): void {
485
- if (this.readyState !== this.OPENED) {
486
- throw new Error('Request has not been opened');
487
- }
488
- this._headers[header.toLowerCase()] = String(value);
489
- }
490
-
491
- /**
492
- * Custom extension for tracking origins of request.
493
- */
494
- setTrackingName(trackingName: string): XMLHttpRequest {
495
- this._trackingName = trackingName;
496
- return this;
497
- }
498
-
499
- /**
500
- * Custom extension for setting a custom performance logger
501
- */
502
- setPerformanceLogger(performanceLogger: IPerformanceLogger): XMLHttpRequest {
503
- this._performanceLogger = performanceLogger;
504
- return this;
505
- }
506
-
507
- open(method: string, url: string, async: ?boolean): void {
508
- /* Other optional arguments are not supported yet */
509
- if (this.readyState !== this.UNSENT) {
510
- throw new Error('Cannot open, already sending');
511
- }
512
- if (async !== undefined && !async) {
513
- // async is default
514
- throw new Error('Synchronous http requests are not supported');
515
- }
516
- if (!url) {
517
- throw new Error('Cannot load an empty url');
518
- }
519
- this._method = method.toUpperCase();
520
- this._url = url;
521
- this._aborted = false;
522
- this.setReadyState(this.OPENED);
523
- }
524
-
525
- send(data: any): void {
526
- if (this.readyState !== this.OPENED) {
527
- throw new Error('Request has not been opened');
528
- }
529
- if (this._sent) {
530
- throw new Error('Request has already been sent');
531
- }
532
- this._sent = true;
533
- const incrementalEvents =
534
- this._incrementalEvents || !!this.onreadystatechange || !!this.onprogress;
535
-
536
- this._subscriptions.push(
537
- RCTNetworking.addListener('didSendNetworkData', args =>
538
- this.__didUploadProgress(...args),
539
- ),
540
- );
541
- this._subscriptions.push(
542
- RCTNetworking.addListener('didReceiveNetworkResponse', args =>
543
- this.__didReceiveResponse(...args),
544
- ),
545
- );
546
- this._subscriptions.push(
547
- RCTNetworking.addListener('didReceiveNetworkData', args =>
548
- this.__didReceiveData(...args),
549
- ),
550
- );
551
- this._subscriptions.push(
552
- RCTNetworking.addListener('didReceiveNetworkIncrementalData', args =>
553
- this.__didReceiveIncrementalData(...args),
554
- ),
555
- );
556
- this._subscriptions.push(
557
- RCTNetworking.addListener('didReceiveNetworkDataProgress', args =>
558
- this.__didReceiveDataProgress(...args),
559
- ),
560
- );
561
- this._subscriptions.push(
562
- RCTNetworking.addListener('didCompleteNetworkResponse', args =>
563
- this.__didCompleteResponse(...args),
564
- ),
565
- );
566
-
567
- let nativeResponseType: NativeResponseType = 'text';
568
- if (this._responseType === 'arraybuffer') {
569
- nativeResponseType = 'base64';
570
- }
571
- if (this._responseType === 'blob') {
572
- nativeResponseType = 'blob';
573
- }
574
-
575
- const doSend = () => {
576
- const friendlyName =
577
- this._trackingName !== 'unknown' ? this._trackingName : this._url;
578
- this._perfKey = 'network_XMLHttpRequest_' + String(friendlyName);
579
- this._performanceLogger.startTimespan(this._perfKey);
580
- invariant(
581
- this._method,
582
- 'XMLHttpRequest method needs to be defined (%s).',
583
- friendlyName,
584
- );
585
- invariant(
586
- this._url,
587
- 'XMLHttpRequest URL needs to be defined (%s).',
588
- friendlyName,
589
- );
590
- RCTNetworking.sendRequest(
591
- this._method,
592
- this._trackingName,
593
- this._url,
594
- this._headers,
595
- data,
596
- /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
597
- * when making Flow check .android.js files. */
598
- nativeResponseType,
599
- incrementalEvents,
600
- this.timeout,
601
- // $FlowFixMe[method-unbinding] added when improving typing for this parameters
602
- this.__didCreateRequest.bind(this),
603
- this.withCredentials,
604
- );
605
- };
606
- if (DEBUG_NETWORK_SEND_DELAY) {
607
- setTimeout(doSend, DEBUG_NETWORK_SEND_DELAY);
608
- } else {
609
- doSend();
610
- }
611
- }
612
-
613
- abort(): void {
614
- this._aborted = true;
615
- if (this._requestId) {
616
- RCTNetworking.abortRequest(this._requestId);
617
- }
618
- // only call onreadystatechange if there is something to abort,
619
- // below logic is per spec
620
- if (
621
- !(
622
- this.readyState === this.UNSENT ||
623
- (this.readyState === this.OPENED && !this._sent) ||
624
- this.readyState === this.DONE
625
- )
626
- ) {
627
- this._reset();
628
- this.setReadyState(this.DONE);
629
- }
630
- // Reset again after, in case modified in handler
631
- this._reset();
632
- }
633
-
634
- setResponseHeaders(responseHeaders: ?Object): void {
635
- this.responseHeaders = responseHeaders || null;
636
- const headers = responseHeaders || {};
637
- this._lowerCaseResponseHeaders = Object.keys(headers).reduce<{
638
- [string]: any,
639
- }>((lcaseHeaders, headerName) => {
640
- lcaseHeaders[headerName.toLowerCase()] = headers[headerName];
641
- return lcaseHeaders;
642
- }, {});
643
- }
644
-
645
- setReadyState(newState: number): void {
646
- this.readyState = newState;
647
- this.dispatchEvent({type: 'readystatechange'});
648
- if (newState === this.DONE) {
649
- if (this._aborted) {
650
- this.dispatchEvent({type: 'abort'});
651
- } else if (this._hasError) {
652
- if (this._timedOut) {
653
- this.dispatchEvent({type: 'timeout'});
654
- } else {
655
- this.dispatchEvent({type: 'error'});
656
- }
657
- } else {
658
- this.dispatchEvent({type: 'load'});
659
- }
660
- this.dispatchEvent({type: 'loadend'});
661
- }
662
- }
663
-
664
- /* global EventListener */
665
- addEventListener(type: string, listener: EventListener): void {
666
- // If we dont' have a 'readystatechange' event handler, we don't
667
- // have to send repeated LOADING events with incremental updates
668
- // to responseText, which will avoid a bunch of native -> JS
669
- // bridge traffic.
670
- if (type === 'readystatechange' || type === 'progress') {
671
- this._incrementalEvents = true;
672
- }
673
- super.addEventListener(type, listener);
674
- }
675
- }
676
-
677
- module.exports = XMLHttpRequest;