@couch-kit/host 0.6.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/package.json +10 -16
- package/src/assets.ts +114 -0
- package/src/index.tsx +5 -4
- package/src/network.ts +2 -6
- package/src/server.ts +8 -5
package/README.md
CHANGED
|
@@ -22,7 +22,28 @@ The server-side library for React Native TV applications. This package turns you
|
|
|
22
22
|
bun add @couch-kit/host
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Then install the required peer dependencies:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx expo install expo-file-system expo-network
|
|
29
|
+
bun add react-native-tcp-socket
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> **Note:** This library requires Expo modules (`expo-file-system`, `expo-network`) and `react-native-tcp-socket` as peer dependencies. These must be installed in your consumer app. React Native's autolinking will handle native setup automatically.
|
|
33
|
+
|
|
34
|
+
## Compatibility
|
|
35
|
+
|
|
36
|
+
| Dependency | Minimum Version |
|
|
37
|
+
| ---------------------------- | --------------- |
|
|
38
|
+
| `react` | `>= 18.2.0` |
|
|
39
|
+
| `react-native` | `>= 0.72.0` |
|
|
40
|
+
| `react-native-nitro-modules` | `>= 0.33.0` |
|
|
41
|
+
| `expo` | `>= 51.0.0` |
|
|
42
|
+
| `expo-file-system` | `>= 17.0.0` |
|
|
43
|
+
| `expo-network` | `>= 7.0.0` |
|
|
44
|
+
| `react-native-tcp-socket` | `>= 6.0.0` |
|
|
45
|
+
|
|
46
|
+
> **New Architecture:** This package supports React Native's New Architecture (Fabric/TurboModules) via React Native 0.83+.
|
|
26
47
|
|
|
27
48
|
## Usage
|
|
28
49
|
|
|
@@ -36,7 +57,7 @@ Config:
|
|
|
36
57
|
- `reducer`: `(state, action) => state` (shared reducer)
|
|
37
58
|
- `port?`: HTTP static server port (default `8080`)
|
|
38
59
|
- `wsPort?`: WebSocket game server port (default `8082`)
|
|
39
|
-
- `staticDir?`: absolute path to the directory of static files to serve.
|
|
60
|
+
- `staticDir?`: absolute path to the directory of static files to serve. **Required on Android** — APK assets live inside a zip archive and cannot be served directly, so use this to point to a writable filesystem path where you've extracted the `www/` assets at runtime. On iOS, defaults to the bundle directory + `/www`.
|
|
40
61
|
- `devMode?`: if true, do not start the TV static file server; instead point phones at `devServerUrl`
|
|
41
62
|
- `devServerUrl?`: URL of your laptop dev server (e.g. `http://192.168.1.50:5173`)
|
|
42
63
|
- `debug?`: enable verbose logs
|
|
@@ -148,6 +169,6 @@ Important: when the controller is served from the laptop, the client-side hook c
|
|
|
148
169
|
|
|
149
170
|
## Bundling / Assets
|
|
150
171
|
|
|
151
|
-
In production, the host serves static controller assets from
|
|
172
|
+
In production, the host serves static controller assets from the iOS bundle directory + `/www` by default. On Android, `staticDir` must be provided since bundle assets live inside the APK.
|
|
152
173
|
|
|
153
174
|
The CLI `couch-kit bundle` copies your web build output into `android/app/src/main/assets/www` (default). Ensure your app packaging makes those assets available under the expected `www` folder.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@couch-kit/host",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "React Native host for local multiplayer party games on Android TV — WebSocket server, state management, and static file serving",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,13 +33,7 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"src",
|
|
35
35
|
"lib",
|
|
36
|
-
"android",
|
|
37
|
-
"ios",
|
|
38
|
-
"cpp",
|
|
39
|
-
"*.podspec",
|
|
40
36
|
"!lib/typescript/example",
|
|
41
|
-
"!android/build",
|
|
42
|
-
"!ios/build",
|
|
43
37
|
"!**/__tests__",
|
|
44
38
|
"!**/__fixtures__",
|
|
45
39
|
"!**/__mocks__"
|
|
@@ -54,22 +48,22 @@
|
|
|
54
48
|
"@couch-kit/core": "0.3.3",
|
|
55
49
|
"buffer": "^6.0.3",
|
|
56
50
|
"js-sha1": "^0.7.0",
|
|
57
|
-
"react-native-
|
|
58
|
-
"react-native-network-info": "^5.2.1",
|
|
59
|
-
"react-native-nitro-http-server": "^1.5.4",
|
|
60
|
-
"react-native-tcp-socket": "^6.0.6"
|
|
51
|
+
"react-native-nitro-http-server": "^1.5.4"
|
|
61
52
|
},
|
|
62
53
|
"devDependencies": {
|
|
63
|
-
"@types/react": "^
|
|
64
|
-
"
|
|
65
|
-
"react": "
|
|
66
|
-
"react-native": "0.72.6",
|
|
54
|
+
"@types/react": "^19.1.1",
|
|
55
|
+
"react": "19.0.4",
|
|
56
|
+
"react-native": "0.83.2",
|
|
67
57
|
"del-cli": "^5.1.0",
|
|
68
58
|
"jest": "^29.7.0"
|
|
69
59
|
},
|
|
70
60
|
"peerDependencies": {
|
|
71
61
|
"react": ">=18.2.0",
|
|
72
62
|
"react-native": ">=0.72.0",
|
|
73
|
-
"react-native-nitro-modules": ">=0.33.0"
|
|
63
|
+
"react-native-nitro-modules": ">=0.33.0",
|
|
64
|
+
"expo": ">=51.0.0",
|
|
65
|
+
"expo-file-system": ">=17.0.0",
|
|
66
|
+
"expo-network": ">=7.0.0",
|
|
67
|
+
"react-native-tcp-socket": ">=6.0.0"
|
|
74
68
|
}
|
|
75
69
|
}
|
package/src/assets.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
documentDirectory,
|
|
4
|
+
getInfoAsync,
|
|
5
|
+
deleteAsync,
|
|
6
|
+
makeDirectoryAsync,
|
|
7
|
+
copyAsync,
|
|
8
|
+
} from "expo-file-system/legacy";
|
|
9
|
+
import { Platform } from "react-native";
|
|
10
|
+
import { toErrorMessage } from "@couch-kit/core";
|
|
11
|
+
|
|
12
|
+
export interface AssetManifest {
|
|
13
|
+
files: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ExtractAssetsResult {
|
|
17
|
+
/** The filesystem path to the extracted www directory, or undefined if not ready */
|
|
18
|
+
staticDir: string | undefined;
|
|
19
|
+
/** Whether extraction is in progress */
|
|
20
|
+
loading: boolean;
|
|
21
|
+
/** Error message if extraction failed */
|
|
22
|
+
error: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extracts bundled web assets from the APK to the filesystem so the native
|
|
27
|
+
* HTTP server can serve them.
|
|
28
|
+
*
|
|
29
|
+
* On Android, assets live inside the APK and cannot be served directly by
|
|
30
|
+
* a native HTTP server. This hook copies each file listed in the manifest
|
|
31
|
+
* from `asset:///www/<file>` to `${documentDirectory}www/<file>`.
|
|
32
|
+
*
|
|
33
|
+
* On iOS, assets are accessible from the bundle directory, so extraction
|
|
34
|
+
* is skipped and `staticDir` is returned as `undefined` (the server falls
|
|
35
|
+
* back to the bundle path).
|
|
36
|
+
*
|
|
37
|
+
* @param manifest - The asset manifest generated by `couch-kit bundle`.
|
|
38
|
+
* Contains a `files` array listing all relative paths.
|
|
39
|
+
*/
|
|
40
|
+
export function useExtractAssets(manifest: AssetManifest): ExtractAssetsResult {
|
|
41
|
+
const [staticDir, setStaticDir] = useState<string | undefined>(undefined);
|
|
42
|
+
const [loading, setLoading] = useState(true);
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
// iOS: skip extraction, server uses bundle path fallback
|
|
47
|
+
if (Platform.OS !== "android") {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let cancelled = false;
|
|
53
|
+
|
|
54
|
+
const extractAssets = async () => {
|
|
55
|
+
try {
|
|
56
|
+
if (!documentDirectory) {
|
|
57
|
+
throw new Error("Document directory is not available");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const targetDir = `${documentDirectory}www/`;
|
|
61
|
+
|
|
62
|
+
// Clean previous extraction to ensure fresh assets after app updates
|
|
63
|
+
const dirInfo = await getInfoAsync(targetDir);
|
|
64
|
+
if (dirInfo.exists) {
|
|
65
|
+
await deleteAsync(targetDir, { idempotent: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create the root target directory
|
|
69
|
+
await makeDirectoryAsync(targetDir, { intermediates: true });
|
|
70
|
+
|
|
71
|
+
// Copy each file from APK assets to filesystem
|
|
72
|
+
for (const file of manifest.files) {
|
|
73
|
+
if (cancelled) return;
|
|
74
|
+
|
|
75
|
+
const sourceUri = `asset:///www/${file}`;
|
|
76
|
+
const destUri = `${targetDir}${file}`;
|
|
77
|
+
|
|
78
|
+
// Ensure subdirectory exists (e.g., "assets/" in "assets/index.js")
|
|
79
|
+
const lastSlash = file.lastIndexOf("/");
|
|
80
|
+
if (lastSlash > 0) {
|
|
81
|
+
const subDir = `${targetDir}${file.substring(0, lastSlash)}`;
|
|
82
|
+
await makeDirectoryAsync(subDir, { intermediates: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await copyAsync({ from: sourceUri, to: destUri });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (cancelled) return;
|
|
89
|
+
|
|
90
|
+
// Strip file:// prefix for the native HTTP server
|
|
91
|
+
const rawPath = targetDir.replace(/^file:\/\//, "");
|
|
92
|
+
setStaticDir(rawPath);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (!cancelled) {
|
|
95
|
+
const message = toErrorMessage(e);
|
|
96
|
+
console.warn("[CouchKit] Asset extraction failed:", message);
|
|
97
|
+
setError(message);
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
if (!cancelled) {
|
|
101
|
+
setLoading(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
extractAssets();
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [manifest]);
|
|
112
|
+
|
|
113
|
+
return { staticDir, loading, error };
|
|
114
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
1
|
+
export * from "./provider";
|
|
2
|
+
export * from "./server";
|
|
3
|
+
export * from "./websocket";
|
|
4
|
+
export * from "./network";
|
|
5
|
+
export * from "./assets";
|
package/src/network.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Network from "expo-network";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Smart IP Discovery
|
|
@@ -7,16 +7,12 @@ import { NetworkInfo } from "react-native-network-info";
|
|
|
7
7
|
*/
|
|
8
8
|
export async function getBestIpAddress(): Promise<string | null> {
|
|
9
9
|
try {
|
|
10
|
-
|
|
11
|
-
const ip = await NetworkInfo.getIPV4Address();
|
|
10
|
+
const ip = await Network.getIpAddressAsync();
|
|
12
11
|
|
|
13
12
|
if (ip && ip !== "0.0.0.0" && ip !== "127.0.0.1") {
|
|
14
13
|
return ip;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
// Fallback logic could go here (e.g., iterating interfaces if exposed by a native module)
|
|
18
|
-
// For now, react-native-network-info is the standard abstraction.
|
|
19
|
-
|
|
20
16
|
return null;
|
|
21
17
|
} catch (error) {
|
|
22
18
|
console.warn("[CouchKit] Failed to get IP address:", error);
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { StaticServer } from "react-native-nitro-http-server";
|
|
3
|
-
import
|
|
3
|
+
import { Paths } from "expo-file-system";
|
|
4
4
|
import { getBestIpAddress } from "./network";
|
|
5
5
|
import { DEFAULT_HTTP_PORT, toErrorMessage } from "@couch-kit/core";
|
|
6
6
|
|
|
@@ -15,7 +15,8 @@ export interface CouchKitHostConfig {
|
|
|
15
15
|
* React hook that manages a static HTTP file server for serving the web controller.
|
|
16
16
|
*
|
|
17
17
|
* In production mode, starts a `StaticServer` bound to `0.0.0.0` on the configured port,
|
|
18
|
-
* serving files from `staticDir` (or
|
|
18
|
+
* serving files from `staticDir` (or the iOS bundle directory + `/www` by default).
|
|
19
|
+
* On Android, `staticDir` must be provided since bundle assets live inside the APK.
|
|
19
20
|
* In dev mode, skips the server and returns `devServerUrl` directly.
|
|
20
21
|
*
|
|
21
22
|
* @param config - Server configuration including port, dev mode, and static directory.
|
|
@@ -48,9 +49,11 @@ export const useStaticServer = (config: CouchKitHostConfig) => {
|
|
|
48
49
|
|
|
49
50
|
// Production Mode: Serve assets from bundle
|
|
50
51
|
try {
|
|
51
|
-
// Use staticDir if provided (required on Android where
|
|
52
|
-
// otherwise fall back to iOS
|
|
53
|
-
const path =
|
|
52
|
+
// Use staticDir if provided (required on Android where bundle path is undefined),
|
|
53
|
+
// otherwise fall back to the iOS bundle directory via expo-file-system
|
|
54
|
+
const path =
|
|
55
|
+
config.staticDir ||
|
|
56
|
+
`${Paths.bundle.uri.replace(/^file:\/\//, "")}www`;
|
|
54
57
|
const port = config.port || DEFAULT_HTTP_PORT;
|
|
55
58
|
|
|
56
59
|
server = new StaticServer();
|