@kccd/expo-http-server 0.1.13
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/.eslintrc.js +5 -0
- package/README.md +114 -0
- package/android/.gradle/8.5/checksums/checksums.lock +0 -0
- package/android/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock +0 -0
- package/android/.gradle/8.5/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.5/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.5/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.5/gc.properties +0 -0
- package/android/.gradle/config.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/.idea/gradle.xml +12 -0
- package/android/.idea/migrations.xml +10 -0
- package/android/.idea/misc.xml +10 -0
- package/android/.idea/other.xml +263 -0
- package/android/build.gradle +94 -0
- package/android/local.properties +8 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/httpserver/ExpoHttpServerModule.kt +107 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoHttpServer.podspec +29 -0
- package/ios/ExpoHttpServerModule.swift +180 -0
- package/package.json +39 -0
- package/src/ExpoHttpServerModule.ts +3 -0
- package/src/index.ts +99 -0
- package/tsconfig.json +9 -0
package/.eslintrc.js
ADDED
package/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
|
2
|
+
# expo-http-server
|
3
|
+
|
4
|
+
[](https://www.npmjs.com/package/expo-http-server)
|
5
|
+
[](https://www.npmjs.com/package/expo-http-server)
|
6
|
+
[](https://github.com/simonsturge/expo-http-server)
|
7
|
+
[](https://github.com/simonsturge/expo-http-server)
|
8
|
+
|
9
|
+
A simple HTTP Server [Expo module](https://docs.expo.dev/modules/) .
|
10
|
+
|
11
|
+
Current implementation is for iOS / Android only ([React Native](https://github.com/facebook/react-native)).
|
12
|
+
|
13
|
+
iOS: [Criollo](https://github.com/thecatalinstan/Criollo)
|
14
|
+
Android: [AndroidServer](https://github.com/fengzhizi715/AndroidServer)
|
15
|
+
Web: **Not implemented**
|
16
|
+
|
17
|
+
## Install
|
18
|
+
|
19
|
+
```shell
|
20
|
+
npx expo install @kccd/expo-http-server
|
21
|
+
```
|
22
|
+
|
23
|
+
## Example
|
24
|
+
```tsx
|
25
|
+
import * as server from "expo-http-server";
|
26
|
+
import { useEffect, useState } from "react";
|
27
|
+
import { Text, View } from "react-native";
|
28
|
+
|
29
|
+
export default function App() {
|
30
|
+
const [lastCalled, setLastCalled] = useState<number | undefined>();
|
31
|
+
|
32
|
+
const html = `
|
33
|
+
<!DOCTYPE html>
|
34
|
+
<html>
|
35
|
+
<body style="background-color:powderblue;">
|
36
|
+
<h1>expo-http-server</h1>
|
37
|
+
<p>You can load HTML!</p>
|
38
|
+
</body>
|
39
|
+
</html>`;
|
40
|
+
|
41
|
+
const obj = { app: "expo-http-server", desc: "You can load JSON!" };
|
42
|
+
|
43
|
+
useEffect(() => {
|
44
|
+
server.setup(9666, (event: server.StatusEvent) => {
|
45
|
+
if (event.status === "ERROR") {
|
46
|
+
// there was an error...
|
47
|
+
} else {
|
48
|
+
// server was STARTED, PAUSED, RESUMED or STOPPED
|
49
|
+
}
|
50
|
+
});
|
51
|
+
server.route("/", "GET", async (request) => {
|
52
|
+
console.log("Request", "/", "GET", request);
|
53
|
+
setLastCalled(Date.now());
|
54
|
+
return {
|
55
|
+
statusCode: 200,
|
56
|
+
headers: {
|
57
|
+
"Custom-Header": "Bazinga",
|
58
|
+
},
|
59
|
+
contentType: "application/json",
|
60
|
+
body: JSON.stringify(obj),
|
61
|
+
};
|
62
|
+
});
|
63
|
+
server.route("/html", "GET", async (request) => {
|
64
|
+
console.log("Request", "/html", "GET", request);
|
65
|
+
setLastCalled(Date.now());
|
66
|
+
return {
|
67
|
+
statusCode: 200,
|
68
|
+
statusDescription: "OK - CUSTOM STATUS",
|
69
|
+
contentType: "text/html",
|
70
|
+
body: html,
|
71
|
+
};
|
72
|
+
});
|
73
|
+
server.start();
|
74
|
+
return () => {
|
75
|
+
server.stop();
|
76
|
+
};
|
77
|
+
}, []);
|
78
|
+
|
79
|
+
return (
|
80
|
+
<View
|
81
|
+
style={{
|
82
|
+
flex: 1,
|
83
|
+
backgroundColor: "#fff",
|
84
|
+
alignItems: "center",
|
85
|
+
justifyContent: "center",
|
86
|
+
}}
|
87
|
+
>
|
88
|
+
<Text>
|
89
|
+
{lastCalled === undefined
|
90
|
+
? "Request webserver to change text"
|
91
|
+
: "Called at " + new Date(lastCalled).toLocaleString()}
|
92
|
+
</Text>
|
93
|
+
</View>
|
94
|
+
);
|
95
|
+
}
|
96
|
+
|
97
|
+
```
|
98
|
+
|
99
|
+
## Running in the background
|
100
|
+
**iOS**: When the app is backgrounded the server will inevitably get paused. There is no getting around this. expo-http-server will start a [background task](https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtask) that should provide a bit more background time, but this will only be ~25 seconds, which could be lowered by Apple in the future. expo-http-server will automatically pause the server when the time runs out, and resume it when the app is resumed.
|
101
|
+
|
102
|
+
**Android**: The server can be ran continuously in the background using a foreground service, e.g. a persistent notification. [Notifee](https://notifee.app/react-native/docs/android/foreground-service#building-a-long-lived-task) can be used to do this. Take a look at the example project for how to set this up.
|
103
|
+
|
104
|
+
## Testing
|
105
|
+
|
106
|
+
Send a request to the server in a browser `browser` or `curl`:
|
107
|
+
|
108
|
+
```shell
|
109
|
+
curl http://IP_OF_DEVICE:MY_PORT
|
110
|
+
```
|
111
|
+
For example:
|
112
|
+
```shell
|
113
|
+
curl http://192.168.1.109:3000
|
114
|
+
```
|
Binary file
|
File without changes
|
Binary file
|
Binary file
|
File without changes
|
File without changes
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<project version="4">
|
3
|
+
<component name="GradleSettings">
|
4
|
+
<option name="linkedExternalProjectsSettings">
|
5
|
+
<GradleProjectSettings>
|
6
|
+
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
7
|
+
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
8
|
+
<option name="resolveExternalAnnotations" value="false" />
|
9
|
+
</GradleProjectSettings>
|
10
|
+
</option>
|
11
|
+
</component>
|
12
|
+
</project>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<project version="4">
|
3
|
+
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
4
|
+
<component name="ProjectRootManager">
|
5
|
+
<output url="file://$PROJECT_DIR$/build/classes" />
|
6
|
+
</component>
|
7
|
+
<component name="ProjectType">
|
8
|
+
<option name="id" value="Android" />
|
9
|
+
</component>
|
10
|
+
</project>
|
@@ -0,0 +1,263 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<project version="4">
|
3
|
+
<component name="direct_access_persist.xml">
|
4
|
+
<option name="deviceSelectionList">
|
5
|
+
<list>
|
6
|
+
<PersistentDeviceSelectionData>
|
7
|
+
<option name="api" value="27" />
|
8
|
+
<option name="brand" value="DOCOMO" />
|
9
|
+
<option name="codename" value="F01L" />
|
10
|
+
<option name="id" value="F01L" />
|
11
|
+
<option name="manufacturer" value="FUJITSU" />
|
12
|
+
<option name="name" value="F-01L" />
|
13
|
+
<option name="screenDensity" value="360" />
|
14
|
+
<option name="screenX" value="720" />
|
15
|
+
<option name="screenY" value="1280" />
|
16
|
+
</PersistentDeviceSelectionData>
|
17
|
+
<PersistentDeviceSelectionData>
|
18
|
+
<option name="api" value="28" />
|
19
|
+
<option name="brand" value="DOCOMO" />
|
20
|
+
<option name="codename" value="SH-01L" />
|
21
|
+
<option name="id" value="SH-01L" />
|
22
|
+
<option name="manufacturer" value="SHARP" />
|
23
|
+
<option name="name" value="AQUOS sense2 SH-01L" />
|
24
|
+
<option name="screenDensity" value="480" />
|
25
|
+
<option name="screenX" value="1080" />
|
26
|
+
<option name="screenY" value="2160" />
|
27
|
+
</PersistentDeviceSelectionData>
|
28
|
+
<PersistentDeviceSelectionData>
|
29
|
+
<option name="api" value="31" />
|
30
|
+
<option name="brand" value="samsung" />
|
31
|
+
<option name="codename" value="a51" />
|
32
|
+
<option name="id" value="a51" />
|
33
|
+
<option name="manufacturer" value="Samsung" />
|
34
|
+
<option name="name" value="Galaxy A51" />
|
35
|
+
<option name="screenDensity" value="420" />
|
36
|
+
<option name="screenX" value="1080" />
|
37
|
+
<option name="screenY" value="2400" />
|
38
|
+
</PersistentDeviceSelectionData>
|
39
|
+
<PersistentDeviceSelectionData>
|
40
|
+
<option name="api" value="34" />
|
41
|
+
<option name="brand" value="google" />
|
42
|
+
<option name="codename" value="akita" />
|
43
|
+
<option name="id" value="akita" />
|
44
|
+
<option name="manufacturer" value="Google" />
|
45
|
+
<option name="name" value="Pixel 8a" />
|
46
|
+
<option name="screenDensity" value="420" />
|
47
|
+
<option name="screenX" value="1080" />
|
48
|
+
<option name="screenY" value="2400" />
|
49
|
+
</PersistentDeviceSelectionData>
|
50
|
+
<PersistentDeviceSelectionData>
|
51
|
+
<option name="api" value="33" />
|
52
|
+
<option name="brand" value="samsung" />
|
53
|
+
<option name="codename" value="b0q" />
|
54
|
+
<option name="id" value="b0q" />
|
55
|
+
<option name="manufacturer" value="Samsung" />
|
56
|
+
<option name="name" value="Galaxy S22 Ultra" />
|
57
|
+
<option name="screenDensity" value="600" />
|
58
|
+
<option name="screenX" value="1440" />
|
59
|
+
<option name="screenY" value="3088" />
|
60
|
+
</PersistentDeviceSelectionData>
|
61
|
+
<PersistentDeviceSelectionData>
|
62
|
+
<option name="api" value="32" />
|
63
|
+
<option name="brand" value="google" />
|
64
|
+
<option name="codename" value="bluejay" />
|
65
|
+
<option name="id" value="bluejay" />
|
66
|
+
<option name="manufacturer" value="Google" />
|
67
|
+
<option name="name" value="Pixel 6a" />
|
68
|
+
<option name="screenDensity" value="420" />
|
69
|
+
<option name="screenX" value="1080" />
|
70
|
+
<option name="screenY" value="2400" />
|
71
|
+
</PersistentDeviceSelectionData>
|
72
|
+
<PersistentDeviceSelectionData>
|
73
|
+
<option name="api" value="29" />
|
74
|
+
<option name="brand" value="samsung" />
|
75
|
+
<option name="codename" value="crownqlteue" />
|
76
|
+
<option name="id" value="crownqlteue" />
|
77
|
+
<option name="manufacturer" value="Samsung" />
|
78
|
+
<option name="name" value="Galaxy Note9" />
|
79
|
+
<option name="screenDensity" value="420" />
|
80
|
+
<option name="screenX" value="2220" />
|
81
|
+
<option name="screenY" value="1080" />
|
82
|
+
</PersistentDeviceSelectionData>
|
83
|
+
<PersistentDeviceSelectionData>
|
84
|
+
<option name="api" value="34" />
|
85
|
+
<option name="brand" value="samsung" />
|
86
|
+
<option name="codename" value="dm3q" />
|
87
|
+
<option name="id" value="dm3q" />
|
88
|
+
<option name="manufacturer" value="Samsung" />
|
89
|
+
<option name="name" value="Galaxy S23 Ultra" />
|
90
|
+
<option name="screenDensity" value="600" />
|
91
|
+
<option name="screenX" value="1440" />
|
92
|
+
<option name="screenY" value="3088" />
|
93
|
+
</PersistentDeviceSelectionData>
|
94
|
+
<PersistentDeviceSelectionData>
|
95
|
+
<option name="api" value="33" />
|
96
|
+
<option name="brand" value="google" />
|
97
|
+
<option name="codename" value="felix" />
|
98
|
+
<option name="id" value="felix" />
|
99
|
+
<option name="manufacturer" value="Google" />
|
100
|
+
<option name="name" value="Pixel Fold" />
|
101
|
+
<option name="screenDensity" value="420" />
|
102
|
+
<option name="screenX" value="2208" />
|
103
|
+
<option name="screenY" value="1840" />
|
104
|
+
</PersistentDeviceSelectionData>
|
105
|
+
<PersistentDeviceSelectionData>
|
106
|
+
<option name="api" value="33" />
|
107
|
+
<option name="brand" value="google" />
|
108
|
+
<option name="codename" value="felix_camera" />
|
109
|
+
<option name="id" value="felix_camera" />
|
110
|
+
<option name="manufacturer" value="Google" />
|
111
|
+
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
112
|
+
<option name="screenDensity" value="420" />
|
113
|
+
<option name="screenX" value="2208" />
|
114
|
+
<option name="screenY" value="1840" />
|
115
|
+
</PersistentDeviceSelectionData>
|
116
|
+
<PersistentDeviceSelectionData>
|
117
|
+
<option name="api" value="33" />
|
118
|
+
<option name="brand" value="samsung" />
|
119
|
+
<option name="codename" value="gts8uwifi" />
|
120
|
+
<option name="id" value="gts8uwifi" />
|
121
|
+
<option name="manufacturer" value="Samsung" />
|
122
|
+
<option name="name" value="Galaxy Tab S8 Ultra" />
|
123
|
+
<option name="screenDensity" value="320" />
|
124
|
+
<option name="screenX" value="1848" />
|
125
|
+
<option name="screenY" value="2960" />
|
126
|
+
</PersistentDeviceSelectionData>
|
127
|
+
<PersistentDeviceSelectionData>
|
128
|
+
<option name="api" value="34" />
|
129
|
+
<option name="brand" value="google" />
|
130
|
+
<option name="codename" value="husky" />
|
131
|
+
<option name="id" value="husky" />
|
132
|
+
<option name="manufacturer" value="Google" />
|
133
|
+
<option name="name" value="Pixel 8 Pro" />
|
134
|
+
<option name="screenDensity" value="390" />
|
135
|
+
<option name="screenX" value="1008" />
|
136
|
+
<option name="screenY" value="2244" />
|
137
|
+
</PersistentDeviceSelectionData>
|
138
|
+
<PersistentDeviceSelectionData>
|
139
|
+
<option name="api" value="30" />
|
140
|
+
<option name="brand" value="motorola" />
|
141
|
+
<option name="codename" value="java" />
|
142
|
+
<option name="id" value="java" />
|
143
|
+
<option name="manufacturer" value="Motorola" />
|
144
|
+
<option name="name" value="G20" />
|
145
|
+
<option name="screenDensity" value="280" />
|
146
|
+
<option name="screenX" value="720" />
|
147
|
+
<option name="screenY" value="1600" />
|
148
|
+
</PersistentDeviceSelectionData>
|
149
|
+
<PersistentDeviceSelectionData>
|
150
|
+
<option name="api" value="33" />
|
151
|
+
<option name="brand" value="google" />
|
152
|
+
<option name="codename" value="lynx" />
|
153
|
+
<option name="id" value="lynx" />
|
154
|
+
<option name="manufacturer" value="Google" />
|
155
|
+
<option name="name" value="Pixel 7a" />
|
156
|
+
<option name="screenDensity" value="420" />
|
157
|
+
<option name="screenX" value="1080" />
|
158
|
+
<option name="screenY" value="2400" />
|
159
|
+
</PersistentDeviceSelectionData>
|
160
|
+
<PersistentDeviceSelectionData>
|
161
|
+
<option name="api" value="31" />
|
162
|
+
<option name="brand" value="google" />
|
163
|
+
<option name="codename" value="oriole" />
|
164
|
+
<option name="id" value="oriole" />
|
165
|
+
<option name="manufacturer" value="Google" />
|
166
|
+
<option name="name" value="Pixel 6" />
|
167
|
+
<option name="screenDensity" value="420" />
|
168
|
+
<option name="screenX" value="1080" />
|
169
|
+
<option name="screenY" value="2400" />
|
170
|
+
</PersistentDeviceSelectionData>
|
171
|
+
<PersistentDeviceSelectionData>
|
172
|
+
<option name="api" value="33" />
|
173
|
+
<option name="brand" value="google" />
|
174
|
+
<option name="codename" value="panther" />
|
175
|
+
<option name="id" value="panther" />
|
176
|
+
<option name="manufacturer" value="Google" />
|
177
|
+
<option name="name" value="Pixel 7" />
|
178
|
+
<option name="screenDensity" value="420" />
|
179
|
+
<option name="screenX" value="1080" />
|
180
|
+
<option name="screenY" value="2400" />
|
181
|
+
</PersistentDeviceSelectionData>
|
182
|
+
<PersistentDeviceSelectionData>
|
183
|
+
<option name="api" value="31" />
|
184
|
+
<option name="brand" value="samsung" />
|
185
|
+
<option name="codename" value="q2q" />
|
186
|
+
<option name="id" value="q2q" />
|
187
|
+
<option name="manufacturer" value="Samsung" />
|
188
|
+
<option name="name" value="Galaxy Z Fold3" />
|
189
|
+
<option name="screenDensity" value="420" />
|
190
|
+
<option name="screenX" value="1768" />
|
191
|
+
<option name="screenY" value="2208" />
|
192
|
+
</PersistentDeviceSelectionData>
|
193
|
+
<PersistentDeviceSelectionData>
|
194
|
+
<option name="api" value="34" />
|
195
|
+
<option name="brand" value="samsung" />
|
196
|
+
<option name="codename" value="q5q" />
|
197
|
+
<option name="id" value="q5q" />
|
198
|
+
<option name="manufacturer" value="Samsung" />
|
199
|
+
<option name="name" value="Galaxy Z Fold5" />
|
200
|
+
<option name="screenDensity" value="420" />
|
201
|
+
<option name="screenX" value="1812" />
|
202
|
+
<option name="screenY" value="2176" />
|
203
|
+
</PersistentDeviceSelectionData>
|
204
|
+
<PersistentDeviceSelectionData>
|
205
|
+
<option name="api" value="30" />
|
206
|
+
<option name="brand" value="google" />
|
207
|
+
<option name="codename" value="r11" />
|
208
|
+
<option name="id" value="r11" />
|
209
|
+
<option name="manufacturer" value="Google" />
|
210
|
+
<option name="name" value="Pixel Watch" />
|
211
|
+
<option name="screenDensity" value="320" />
|
212
|
+
<option name="screenX" value="384" />
|
213
|
+
<option name="screenY" value="384" />
|
214
|
+
<option name="type" value="WEAR_OS" />
|
215
|
+
</PersistentDeviceSelectionData>
|
216
|
+
<PersistentDeviceSelectionData>
|
217
|
+
<option name="api" value="30" />
|
218
|
+
<option name="brand" value="google" />
|
219
|
+
<option name="codename" value="redfin" />
|
220
|
+
<option name="id" value="redfin" />
|
221
|
+
<option name="manufacturer" value="Google" />
|
222
|
+
<option name="name" value="Pixel 5" />
|
223
|
+
<option name="screenDensity" value="440" />
|
224
|
+
<option name="screenX" value="1080" />
|
225
|
+
<option name="screenY" value="2340" />
|
226
|
+
</PersistentDeviceSelectionData>
|
227
|
+
<PersistentDeviceSelectionData>
|
228
|
+
<option name="api" value="34" />
|
229
|
+
<option name="brand" value="google" />
|
230
|
+
<option name="codename" value="shiba" />
|
231
|
+
<option name="id" value="shiba" />
|
232
|
+
<option name="manufacturer" value="Google" />
|
233
|
+
<option name="name" value="Pixel 8" />
|
234
|
+
<option name="screenDensity" value="420" />
|
235
|
+
<option name="screenX" value="1080" />
|
236
|
+
<option name="screenY" value="2400" />
|
237
|
+
</PersistentDeviceSelectionData>
|
238
|
+
<PersistentDeviceSelectionData>
|
239
|
+
<option name="api" value="33" />
|
240
|
+
<option name="brand" value="google" />
|
241
|
+
<option name="codename" value="tangorpro" />
|
242
|
+
<option name="id" value="tangorpro" />
|
243
|
+
<option name="manufacturer" value="Google" />
|
244
|
+
<option name="name" value="Pixel Tablet" />
|
245
|
+
<option name="screenDensity" value="320" />
|
246
|
+
<option name="screenX" value="1600" />
|
247
|
+
<option name="screenY" value="2560" />
|
248
|
+
</PersistentDeviceSelectionData>
|
249
|
+
<PersistentDeviceSelectionData>
|
250
|
+
<option name="api" value="29" />
|
251
|
+
<option name="brand" value="samsung" />
|
252
|
+
<option name="codename" value="x1q" />
|
253
|
+
<option name="id" value="x1q" />
|
254
|
+
<option name="manufacturer" value="Samsung" />
|
255
|
+
<option name="name" value="Galaxy S20" />
|
256
|
+
<option name="screenDensity" value="480" />
|
257
|
+
<option name="screenX" value="1440" />
|
258
|
+
<option name="screenY" value="3200" />
|
259
|
+
</PersistentDeviceSelectionData>
|
260
|
+
</list>
|
261
|
+
</option>
|
262
|
+
</component>
|
263
|
+
</project>
|
@@ -0,0 +1,94 @@
|
|
1
|
+
apply plugin: 'com.android.library'
|
2
|
+
apply plugin: 'kotlin-android'
|
3
|
+
apply plugin: 'maven-publish'
|
4
|
+
|
5
|
+
group = 'expo.modules.httpserver'
|
6
|
+
version = '0.1.0'
|
7
|
+
|
8
|
+
buildscript {
|
9
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
10
|
+
if (expoModulesCorePlugin.exists()) {
|
11
|
+
apply from: expoModulesCorePlugin
|
12
|
+
applyKotlinExpoModulesCorePlugin()
|
13
|
+
}
|
14
|
+
|
15
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
16
|
+
ext.safeExtGet = { prop, fallback ->
|
17
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
18
|
+
}
|
19
|
+
|
20
|
+
// Ensures backward compatibility
|
21
|
+
ext.getKotlinVersion = {
|
22
|
+
if (ext.has("kotlinVersion")) {
|
23
|
+
ext.kotlinVersion()
|
24
|
+
} else {
|
25
|
+
ext.safeExtGet("kotlinVersion", "1.8.10")
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
repositories {
|
30
|
+
mavenCentral()
|
31
|
+
}
|
32
|
+
|
33
|
+
dependencies {
|
34
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
afterEvaluate {
|
39
|
+
publishing {
|
40
|
+
publications {
|
41
|
+
release(MavenPublication) {
|
42
|
+
from components.release
|
43
|
+
}
|
44
|
+
}
|
45
|
+
repositories {
|
46
|
+
maven {
|
47
|
+
url = mavenLocal().url
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
android {
|
54
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 33)
|
55
|
+
|
56
|
+
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
|
57
|
+
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
|
58
|
+
compileOptions {
|
59
|
+
sourceCompatibility JavaVersion.VERSION_11
|
60
|
+
targetCompatibility JavaVersion.VERSION_11
|
61
|
+
}
|
62
|
+
|
63
|
+
kotlinOptions {
|
64
|
+
jvmTarget = JavaVersion.VERSION_11.majorVersion
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
namespace "expo.modules.httpserver"
|
69
|
+
defaultConfig {
|
70
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
71
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
72
|
+
versionCode 1
|
73
|
+
versionName "0.1.0"
|
74
|
+
}
|
75
|
+
lintOptions {
|
76
|
+
abortOnError false
|
77
|
+
}
|
78
|
+
publishing {
|
79
|
+
singleVariant("release") {
|
80
|
+
withSourcesJar()
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
repositories {
|
86
|
+
mavenCentral()
|
87
|
+
}
|
88
|
+
|
89
|
+
dependencies {
|
90
|
+
implementation project(':expo-modules-core')
|
91
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
92
|
+
implementation 'com.github.fengzhizi715.AndroidServer:core:1.3.3'
|
93
|
+
implementation 'com.github.fengzhizi715.AndroidServer:gson:1.3.3'
|
94
|
+
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
## This file must *NOT* be checked into Version Control Systems,
|
2
|
+
# as it contains information specific to your local configuration.
|
3
|
+
#
|
4
|
+
# Location of the SDK. This is only used by Gradle.
|
5
|
+
# For customization when using a Version Control System, please read the
|
6
|
+
# header note.
|
7
|
+
#Wed Jul 17 17:56:56 CST 2024
|
8
|
+
sdk.dir=/home/pxgo/Android/Sdk
|
@@ -0,0 +1,107 @@
|
|
1
|
+
package expo.modules.httpserver
|
2
|
+
|
3
|
+
import androidx.core.os.bundleOf
|
4
|
+
import expo.modules.kotlin.modules.Module
|
5
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
6
|
+
import com.safframework.server.core.AndroidServer
|
7
|
+
import com.safframework.server.core.Server
|
8
|
+
import com.safframework.server.core.http.HttpMethod
|
9
|
+
import com.safframework.server.core.http.Request
|
10
|
+
import com.safframework.server.core.http.Response
|
11
|
+
import org.json.JSONObject
|
12
|
+
import java.util.UUID
|
13
|
+
|
14
|
+
class ExpoHttpServerModule : Module() {
|
15
|
+
class SimpleHttpResponse(val statusCode: Int,
|
16
|
+
val statusDescription: String,
|
17
|
+
val contentType: String,
|
18
|
+
val headers: HashMap<String, String>,
|
19
|
+
val body: String)
|
20
|
+
|
21
|
+
private var server: Server? = null;
|
22
|
+
private var started = false;
|
23
|
+
private val responses = HashMap<String, SimpleHttpResponse>()
|
24
|
+
|
25
|
+
override fun definition() = ModuleDefinition {
|
26
|
+
|
27
|
+
Name("ExpoHttpServer")
|
28
|
+
|
29
|
+
Events("onStatusUpdate", "onRequest")
|
30
|
+
|
31
|
+
Function("setup") { port: Int ->
|
32
|
+
server = AndroidServer.Builder{
|
33
|
+
port {
|
34
|
+
port
|
35
|
+
}
|
36
|
+
}.build()
|
37
|
+
}
|
38
|
+
|
39
|
+
Function("route") { path: String, method: String, uuid: String ->
|
40
|
+
server = server?.request(HttpMethod.getMethod(method), path) { request: Request, response: Response ->
|
41
|
+
val headers: Map<String, String> = request.headers()
|
42
|
+
val params: Map<String, String> = request.params()
|
43
|
+
val cookies: Map<String, String> = request.cookies().associate { it.name() to it.value() }
|
44
|
+
val requestId = UUID.randomUUID().toString()
|
45
|
+
sendEvent("onRequest", bundleOf(
|
46
|
+
"uuid" to uuid,
|
47
|
+
"requestId" to requestId,
|
48
|
+
"method" to request.method().name,
|
49
|
+
"path" to request.url(),
|
50
|
+
"body" to request.content(),
|
51
|
+
"headersJson" to JSONObject(headers).toString(),
|
52
|
+
"paramsJson" to JSONObject(params).toString(),
|
53
|
+
"cookiesJson" to JSONObject(cookies).toString(),
|
54
|
+
))
|
55
|
+
while (!responses.containsKey(requestId)) {
|
56
|
+
Thread.sleep(10)
|
57
|
+
}
|
58
|
+
val res = responses[requestId]!!
|
59
|
+
response.setBodyText(res.body)
|
60
|
+
response.setStatus(res.statusCode)
|
61
|
+
response.addHeader("Content-Length", "" + res.body.length)
|
62
|
+
response.addHeader("Content-Type", res.contentType)
|
63
|
+
for ((key, value) in res.headers) {
|
64
|
+
response.addHeader(key, value)
|
65
|
+
}
|
66
|
+
responses.remove(requestId);
|
67
|
+
return@request response
|
68
|
+
};
|
69
|
+
}
|
70
|
+
|
71
|
+
Function("start") {
|
72
|
+
if (server == null) {
|
73
|
+
sendEvent("onStatusUpdate", bundleOf(
|
74
|
+
"status" to "ERROR",
|
75
|
+
"message" to "Server not setup / port not configured"
|
76
|
+
))
|
77
|
+
} else {
|
78
|
+
if (!started) {
|
79
|
+
started = true
|
80
|
+
server?.start()
|
81
|
+
sendEvent("onStatusUpdate", bundleOf(
|
82
|
+
"status" to "STARTED",
|
83
|
+
"message" to "Server started"
|
84
|
+
))
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
Function("respond") { requestId: String,
|
90
|
+
statusCode: Int,
|
91
|
+
statusDescription: String,
|
92
|
+
contentType: String,
|
93
|
+
headers: HashMap<String, String>,
|
94
|
+
body: String ->
|
95
|
+
responses[requestId] = SimpleHttpResponse(statusCode, statusDescription, contentType, headers, body);
|
96
|
+
}
|
97
|
+
|
98
|
+
Function("stop") {
|
99
|
+
started = false
|
100
|
+
server?.close()
|
101
|
+
sendEvent("onStatusUpdate", bundleOf(
|
102
|
+
"status" to "STOPPED",
|
103
|
+
"message" to "Server stopped"
|
104
|
+
))
|
105
|
+
}
|
106
|
+
}
|
107
|
+
}
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
4
|
+
|
5
|
+
Pod::Spec.new do |s|
|
6
|
+
s.name = 'ExpoHttpServer'
|
7
|
+
s.version = package['version']
|
8
|
+
s.summary = package['description']
|
9
|
+
s.description = package['description']
|
10
|
+
s.license = package['license']
|
11
|
+
s.author = package['author']
|
12
|
+
s.homepage = package['homepage']
|
13
|
+
s.platform = :ios, '13.4'
|
14
|
+
s.swift_version = '5.4'
|
15
|
+
s.source = { git: 'https://github.com/simonsturge/expo-http-server' }
|
16
|
+
s.static_framework = true
|
17
|
+
|
18
|
+
s.dependency 'ExpoModulesCore'
|
19
|
+
s.dependency 'Criollo'
|
20
|
+
s.dependency 'CocoaAsyncSocket'
|
21
|
+
|
22
|
+
# Swift/Objective-C compatibility
|
23
|
+
s.pod_target_xcconfig = {
|
24
|
+
'DEFINES_MODULE' => 'YES',
|
25
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
26
|
+
}
|
27
|
+
|
28
|
+
s.source_files = "**/*.{h,m,swift}"
|
29
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
import ExpoModulesCore
|
2
|
+
import Criollo
|
3
|
+
import Foundation
|
4
|
+
|
5
|
+
public class ExpoHttpServerModule: Module {
|
6
|
+
private let server = CRHTTPServer()
|
7
|
+
private var port: Int?
|
8
|
+
private var stopped = false
|
9
|
+
private var responses = [String: CRResponse]()
|
10
|
+
private var bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
11
|
+
|
12
|
+
public func definition() -> ModuleDefinition {
|
13
|
+
Name("ExpoHttpServer")
|
14
|
+
|
15
|
+
Events("onStatusUpdate", "onRequest")
|
16
|
+
|
17
|
+
Function("setup", setupHandler)
|
18
|
+
Function("start", startHandler)
|
19
|
+
Function("route", routeHandler)
|
20
|
+
Function("respond", respondHandler)
|
21
|
+
Function("stop", stopHandler)
|
22
|
+
}
|
23
|
+
|
24
|
+
private func setupHandler(port: Int) {
|
25
|
+
self.port = port;
|
26
|
+
}
|
27
|
+
|
28
|
+
private func startHandler() {
|
29
|
+
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [unowned self] notification in
|
30
|
+
if (!self.stopped) {
|
31
|
+
self.startServer(status: "RESUMED", message: "Server resumed")
|
32
|
+
}
|
33
|
+
}
|
34
|
+
stopped = false;
|
35
|
+
startServer(status: "STARTED", message: "Server started")
|
36
|
+
}
|
37
|
+
|
38
|
+
private func routeHandler(path: String, method: String, uuid: String) {
|
39
|
+
server.add(path, block: { (req, res, next) in
|
40
|
+
DispatchQueue.main.async {
|
41
|
+
var bodyString = "{}"
|
42
|
+
if let body = req.body, let bodyData = try? JSONSerialization.data(withJSONObject: body) {
|
43
|
+
bodyString = String(data: bodyData, encoding: .utf8) ?? "{}"
|
44
|
+
}
|
45
|
+
let requestId = UUID().uuidString
|
46
|
+
self.responses[requestId] = res
|
47
|
+
self.sendEvent("onRequest", [
|
48
|
+
"uuid": uuid,
|
49
|
+
"requestId": requestId,
|
50
|
+
"method": req.method.toString(),
|
51
|
+
"path": path,
|
52
|
+
"body": bodyString,
|
53
|
+
"headersJson": req.allHTTPHeaderFields.jsonString,
|
54
|
+
"paramsJson": req.query.jsonString,
|
55
|
+
"cookiesJson": req.cookies?.jsonString ?? "{}"
|
56
|
+
])
|
57
|
+
}
|
58
|
+
}, recursive: false, method: CRHTTPMethod.fromString(method))
|
59
|
+
}
|
60
|
+
|
61
|
+
private func respondHandler(requestId: String,
|
62
|
+
statusCode: Int,
|
63
|
+
statusDescription: String,
|
64
|
+
contentType: String,
|
65
|
+
headers: [String: String],
|
66
|
+
body: String) {
|
67
|
+
DispatchQueue.main.async {
|
68
|
+
if let response = self.responses[requestId] {
|
69
|
+
response.setStatusCode(UInt(statusCode), description: statusDescription)
|
70
|
+
response.setValue(contentType, forHTTPHeaderField: "Content-type")
|
71
|
+
response.setValue("\(body.count)", forHTTPHeaderField: "Content-Length")
|
72
|
+
for (key, value) in headers {
|
73
|
+
response.setValue(value, forHTTPHeaderField: key)
|
74
|
+
}
|
75
|
+
response.send(body);
|
76
|
+
self.responses[requestId] = nil;
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
}
|
81
|
+
|
82
|
+
private func stopHandler() {
|
83
|
+
stopped = true;
|
84
|
+
stopServer(status: "STOPPED", message: "Server stopped")
|
85
|
+
}
|
86
|
+
|
87
|
+
private func startServer(status: String, message: String) {
|
88
|
+
stopServer()
|
89
|
+
if let port = port {
|
90
|
+
var error: NSError?
|
91
|
+
server.startListening(&error, portNumber: UInt(port))
|
92
|
+
if (error != nil) {
|
93
|
+
sendEvent("onStatusUpdate", [
|
94
|
+
"status": "ERROR",
|
95
|
+
"message": error?.localizedDescription ?? "Unknown error starting server"
|
96
|
+
])
|
97
|
+
} else {
|
98
|
+
beginBackgroundTask()
|
99
|
+
sendEvent("onStatusUpdate", [
|
100
|
+
"status": status,
|
101
|
+
"message": message
|
102
|
+
])
|
103
|
+
}
|
104
|
+
} else {
|
105
|
+
sendEvent("onStatusUpdate", [
|
106
|
+
"status": "ERROR",
|
107
|
+
"message": "Can't start server with port configured"
|
108
|
+
])
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
private func stopServer(status: String? = nil, message: String? = nil) {
|
113
|
+
server.stopListening()
|
114
|
+
endBackgroundTask()
|
115
|
+
if let status = status, let message = message {
|
116
|
+
sendEvent("onStatusUpdate", [
|
117
|
+
"status": status,
|
118
|
+
"message": message
|
119
|
+
])
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
private func beginBackgroundTask() {
|
124
|
+
if (bgTaskIdentifier == UIBackgroundTaskIdentifier.invalid) {
|
125
|
+
self.bgTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "BgTask", expirationHandler: {
|
126
|
+
self.stopServer(status: "PAUSED", message: "Server paused")
|
127
|
+
})
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
private func endBackgroundTask() {
|
132
|
+
if (bgTaskIdentifier != UIBackgroundTaskIdentifier.invalid) {
|
133
|
+
UIApplication.shared.endBackgroundTask(bgTaskIdentifier)
|
134
|
+
bgTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
extension Dictionary {
|
140
|
+
var jsonString: String {
|
141
|
+
guard let data = try? JSONSerialization.data(withJSONObject: self) else {
|
142
|
+
return "{}";
|
143
|
+
}
|
144
|
+
return String(data: data, encoding: .utf8) ?? "{}"
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
extension CRHTTPMethod {
|
149
|
+
func toString() -> String {
|
150
|
+
switch self {
|
151
|
+
case .post:
|
152
|
+
return "POST"
|
153
|
+
case .put:
|
154
|
+
return "PUT"
|
155
|
+
case .delete:
|
156
|
+
return "DELETE"
|
157
|
+
case .options:
|
158
|
+
return "OPTIONS"
|
159
|
+
default:
|
160
|
+
return "GET"
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
static func fromString(_ string: String) -> Self {
|
165
|
+
var httpMethod: CRHTTPMethod
|
166
|
+
switch (string) {
|
167
|
+
case "POST":
|
168
|
+
httpMethod = .post
|
169
|
+
case "PUT":
|
170
|
+
httpMethod = .put
|
171
|
+
case "DELETE":
|
172
|
+
httpMethod = .delete
|
173
|
+
case "OPTIONS":
|
174
|
+
httpMethod = .options
|
175
|
+
default:
|
176
|
+
httpMethod = .get
|
177
|
+
}
|
178
|
+
return httpMethod
|
179
|
+
}
|
180
|
+
}
|
package/package.json
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
{
|
2
|
+
"name": "@kccd/expo-http-server",
|
3
|
+
"version": "0.1.13",
|
4
|
+
"description": "A simple HTTP server expo module (iOS/Android)",
|
5
|
+
"main": "build/index.js",
|
6
|
+
"types": "build/index.d.ts",
|
7
|
+
"scripts": {
|
8
|
+
"build": "expo-module build",
|
9
|
+
"clean": "expo-module clean",
|
10
|
+
"lint": "expo-module lint",
|
11
|
+
"test": "expo-module test",
|
12
|
+
"open:ios": "open -a \"Xcode\" example/ios",
|
13
|
+
"open:android": "open -a \"Android Studio\" example/android"
|
14
|
+
},
|
15
|
+
"keywords": [
|
16
|
+
"react-native",
|
17
|
+
"expo",
|
18
|
+
"expo-http-server",
|
19
|
+
"ExpoHttpServer"
|
20
|
+
],
|
21
|
+
"repository": "https://github.com/simonsturge/expo-http-server",
|
22
|
+
"bugs": {
|
23
|
+
"url": "https://github.com/simonsturge/expo-http-server/issues"
|
24
|
+
},
|
25
|
+
"author": "Simon <development@simonsturge.com> (https://github.com/simonsturge)",
|
26
|
+
"license": "MIT",
|
27
|
+
"homepage": "https://github.com/simonsturge/expo-http-server#readme",
|
28
|
+
"dependencies": {},
|
29
|
+
"devDependencies": {
|
30
|
+
"@types/react": "^18.0.25",
|
31
|
+
"expo-module-scripts": "^3.4.0",
|
32
|
+
"expo-modules-core": "^1.11.8"
|
33
|
+
},
|
34
|
+
"peerDependencies": {
|
35
|
+
"expo": "*",
|
36
|
+
"react": "*",
|
37
|
+
"react-native": "*"
|
38
|
+
}
|
39
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
import { EventEmitter } from "expo-modules-core";
|
2
|
+
|
3
|
+
import ExpoHttpServerModule from "./ExpoHttpServerModule";
|
4
|
+
|
5
|
+
const emitter = new EventEmitter(ExpoHttpServerModule);
|
6
|
+
const requestCallbacks: Callback[] = [];
|
7
|
+
|
8
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS";
|
9
|
+
/**
|
10
|
+
* PAUSED AND RESUMED are iOS only
|
11
|
+
*/
|
12
|
+
export type Status = "STARTED" | "PAUSED" | "RESUMED" | "STOPPED" | "ERROR";
|
13
|
+
|
14
|
+
export interface StatusEvent {
|
15
|
+
status: Status;
|
16
|
+
message: string;
|
17
|
+
}
|
18
|
+
|
19
|
+
export interface RequestEvent {
|
20
|
+
uuid: string;
|
21
|
+
requestId: string;
|
22
|
+
method: string;
|
23
|
+
path: string;
|
24
|
+
body: string;
|
25
|
+
headersJson: string;
|
26
|
+
paramsJson: string;
|
27
|
+
cookiesJson: string;
|
28
|
+
}
|
29
|
+
|
30
|
+
export interface Response {
|
31
|
+
statusCode?: number;
|
32
|
+
statusDescription?: string;
|
33
|
+
contentType?: string;
|
34
|
+
headers?: Record<string, string>;
|
35
|
+
body?: string;
|
36
|
+
}
|
37
|
+
|
38
|
+
export interface Callback {
|
39
|
+
method: string;
|
40
|
+
path: string;
|
41
|
+
uuid: string;
|
42
|
+
callback: (request: RequestEvent) => Promise<Response>;
|
43
|
+
}
|
44
|
+
|
45
|
+
export const start = () => {
|
46
|
+
emitter.addListener<RequestEvent>("onRequest", async (event) => {
|
47
|
+
const responseHandler = requestCallbacks.find((c) => c.uuid === event.uuid);
|
48
|
+
if (!responseHandler) {
|
49
|
+
ExpoHttpServerModule.respond(
|
50
|
+
event.requestId,
|
51
|
+
404,
|
52
|
+
"Not Found",
|
53
|
+
"application/json",
|
54
|
+
{},
|
55
|
+
JSON.stringify({ error: "Handler not found" }),
|
56
|
+
);
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
const response = await responseHandler.callback(event);
|
60
|
+
ExpoHttpServerModule.respond(
|
61
|
+
event.requestId,
|
62
|
+
response.statusCode || 200,
|
63
|
+
response.statusDescription || "OK",
|
64
|
+
response.contentType || "application/json",
|
65
|
+
response.headers || {},
|
66
|
+
response.body || "{}",
|
67
|
+
);
|
68
|
+
});
|
69
|
+
ExpoHttpServerModule.start();
|
70
|
+
};
|
71
|
+
|
72
|
+
export const route = (
|
73
|
+
path: string,
|
74
|
+
method: HttpMethod,
|
75
|
+
callback: (request: RequestEvent) => Promise<Response>,
|
76
|
+
) => {
|
77
|
+
const uuid = Math.random().toString(16).slice(2);
|
78
|
+
requestCallbacks.push({
|
79
|
+
method,
|
80
|
+
path,
|
81
|
+
uuid,
|
82
|
+
callback,
|
83
|
+
});
|
84
|
+
ExpoHttpServerModule.route(path, method, uuid);
|
85
|
+
};
|
86
|
+
|
87
|
+
export const setup = (
|
88
|
+
port: number,
|
89
|
+
onStatusUpdate?: (event: StatusEvent) => void,
|
90
|
+
) => {
|
91
|
+
if (onStatusUpdate) {
|
92
|
+
emitter.addListener<StatusEvent>("onStatusUpdate", async (event) => {
|
93
|
+
onStatusUpdate(event);
|
94
|
+
});
|
95
|
+
}
|
96
|
+
ExpoHttpServerModule.setup(port);
|
97
|
+
};
|
98
|
+
|
99
|
+
export const stop = () => ExpoHttpServerModule.stop();
|