@rubriclab/bunl 0.0.11 → 0.0.12
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 +14 -1
- package/bun.lockb +0 -0
- package/client.ts +45 -13
- package/package.json +4 -3
- package/server.ts +21 -10
package/README.md
CHANGED
|
@@ -39,9 +39,22 @@ bun client -p 3000
|
|
|
39
39
|
With full args:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
bun client --port 3000 --domain
|
|
42
|
+
bun client --port 3000 --domain example.so --subdomain my-subdomain --open
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
Or in shortform:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bun client -p 3000 -d example.so -s my-subdomain -o
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The options:
|
|
52
|
+
|
|
53
|
+
- `port` / `p` the localhost port to expose eg. **3000**
|
|
54
|
+
- `domain` / `d` the hostname of the server Bunl is running on eg. **example.so**
|
|
55
|
+
- `subdomain` / `s` the public URL to request eg. **my-subdomain**.example.so
|
|
56
|
+
- `open` / `o` to auto-open your public URL in the browser
|
|
57
|
+
|
|
45
58
|
### [WIP] Deployment
|
|
46
59
|
|
|
47
60
|
To build the client code:
|
package/bun.lockb
CHANGED
|
Binary file
|
package/client.ts
CHANGED
|
@@ -1,41 +1,65 @@
|
|
|
1
1
|
import { parseArgs } from "util";
|
|
2
|
+
import browser from "open";
|
|
2
3
|
|
|
3
4
|
async function main({
|
|
4
5
|
url,
|
|
5
6
|
domain,
|
|
6
7
|
subdomain,
|
|
8
|
+
open,
|
|
7
9
|
}: {
|
|
8
10
|
url: string;
|
|
9
11
|
domain?: string;
|
|
10
12
|
subdomain?: string;
|
|
13
|
+
open?: boolean;
|
|
11
14
|
}) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const params = new URLSearchParams({
|
|
16
|
+
new: "",
|
|
17
|
+
...(subdomain ? { subdomain } : {}),
|
|
18
|
+
}).toString();
|
|
19
|
+
const serverUrl = `ws://${domain}?${params}`;
|
|
15
20
|
const socket = new WebSocket(serverUrl);
|
|
16
21
|
|
|
17
|
-
socket.addEventListener("message", (event) => {
|
|
22
|
+
socket.addEventListener("message", async (event) => {
|
|
18
23
|
const data = JSON.parse(event.data as string);
|
|
19
24
|
console.log("message:", data);
|
|
20
25
|
|
|
26
|
+
if (open && data.url) browser(data.url);
|
|
27
|
+
|
|
21
28
|
if (data.method) {
|
|
22
|
-
fetch(`${url}${data.
|
|
29
|
+
const res = await fetch(`${url}${data.pathname}`, {
|
|
23
30
|
method: data.method,
|
|
24
31
|
headers: data.headers,
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const { status, statusText, headers } = res;
|
|
35
|
+
const body = await res.text();
|
|
36
|
+
|
|
37
|
+
const serializedRes = JSON.stringify({
|
|
38
|
+
pathname: data.pathname,
|
|
39
|
+
status,
|
|
40
|
+
statusText,
|
|
41
|
+
headers: Object.fromEntries(headers),
|
|
42
|
+
body,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
socket.send(serializedRes);
|
|
28
46
|
}
|
|
29
47
|
});
|
|
30
48
|
|
|
31
49
|
socket.addEventListener("open", (event) => {
|
|
32
50
|
if (!(event.target as any).readyState) throw "Not ready";
|
|
33
51
|
});
|
|
52
|
+
|
|
53
|
+
socket.addEventListener("close", () => {
|
|
54
|
+
console.log(`\x1b[31mfailed to connect to server\x1b[0m`);
|
|
55
|
+
process.exit();
|
|
56
|
+
});
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
/**
|
|
37
|
-
* Eg. `bun client.ts -p 3000 -d
|
|
38
|
-
* > my-subdomain.
|
|
60
|
+
* Eg. `bun client.ts -p 3000 -d example.so -s my-subdomain -o`
|
|
61
|
+
* > my-subdomain.example.so will be proxied to localhost:3000
|
|
62
|
+
* See README for full usage.
|
|
39
63
|
*/
|
|
40
64
|
const { values } = parseArgs({
|
|
41
65
|
args: Bun.argv,
|
|
@@ -47,20 +71,28 @@ const { values } = parseArgs({
|
|
|
47
71
|
},
|
|
48
72
|
domain: {
|
|
49
73
|
type: "string",
|
|
74
|
+
default: "localhost:1234",
|
|
50
75
|
short: "d",
|
|
51
76
|
},
|
|
52
77
|
subdomain: {
|
|
53
78
|
type: "string",
|
|
54
79
|
short: "s",
|
|
55
80
|
},
|
|
81
|
+
open: {
|
|
82
|
+
type: "boolean",
|
|
83
|
+
short: "o",
|
|
84
|
+
},
|
|
56
85
|
},
|
|
57
86
|
allowPositionals: true,
|
|
58
87
|
});
|
|
59
88
|
|
|
60
89
|
if (!values.port) throw "pass -p 3000";
|
|
61
90
|
|
|
91
|
+
const { port, domain, subdomain, open } = values;
|
|
92
|
+
|
|
62
93
|
main({
|
|
63
|
-
url: `localhost:${
|
|
64
|
-
domain
|
|
65
|
-
subdomain
|
|
94
|
+
url: `localhost:${port}`,
|
|
95
|
+
domain,
|
|
96
|
+
subdomain,
|
|
97
|
+
open,
|
|
66
98
|
});
|
package/package.json
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
"bunl": "build/client.js"
|
|
4
4
|
},
|
|
5
5
|
"name": "@rubriclab/bunl",
|
|
6
|
-
"description": "
|
|
7
|
-
"version": "0.0.
|
|
6
|
+
"description": "Expose localhost to the world",
|
|
7
|
+
"version": "0.0.12",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/RubricLab/bunl.git"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"human-id": "^4.1.1"
|
|
14
|
+
"human-id": "^4.1.1",
|
|
15
|
+
"open": "^10.1.0"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
18
|
"@types/bun": "latest"
|
package/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ const port = Bun.env.PORT || 1234;
|
|
|
7
7
|
const scheme = Bun.env.SCHEME || "http";
|
|
8
8
|
const domain = Bun.env.DOMAIN || `localhost:${port}`;
|
|
9
9
|
|
|
10
|
+
// TODO: replace this with Redis to preserve sessions across deployments
|
|
10
11
|
const clients = new Map<string, ServerWebSocket<Client>>();
|
|
11
12
|
const clientData = new Map<string, any>();
|
|
12
13
|
|
|
@@ -17,7 +18,9 @@ serve<Client>({
|
|
|
17
18
|
|
|
18
19
|
if (reqUrl.searchParams.has("new")) {
|
|
19
20
|
const requested = reqUrl.searchParams.get("subdomain");
|
|
20
|
-
|
|
21
|
+
let id = requested || uid();
|
|
22
|
+
if (clients.has(id)) id = uid();
|
|
23
|
+
|
|
21
24
|
const upgraded = server.upgrade(req, { data: { id } });
|
|
22
25
|
if (upgraded) return;
|
|
23
26
|
else return new Response("upgrade failed", { status: 500 });
|
|
@@ -32,14 +35,14 @@ serve<Client>({
|
|
|
32
35
|
// The magic: forward the req to the client
|
|
33
36
|
const client = clients.get(subdomain)!;
|
|
34
37
|
const { method, url, headers } = req;
|
|
35
|
-
const
|
|
36
|
-
client.send(JSON.stringify({ method,
|
|
38
|
+
const { pathname } = new URL(url);
|
|
39
|
+
client.send(JSON.stringify({ method, pathname, headers }));
|
|
37
40
|
|
|
38
41
|
// Wait for the client to cache its response above
|
|
39
42
|
await sleep(1);
|
|
40
43
|
|
|
41
44
|
let retries = 5;
|
|
42
|
-
let res = clientData.get(subdomain);
|
|
45
|
+
let res = clientData.get(`${subdomain}/${pathname}`);
|
|
43
46
|
|
|
44
47
|
// Poll every second for the client to respond
|
|
45
48
|
// TODO: replace poll with a client-triggered callback
|
|
@@ -47,20 +50,26 @@ serve<Client>({
|
|
|
47
50
|
await sleep(1000);
|
|
48
51
|
retries--;
|
|
49
52
|
|
|
50
|
-
res = clientData.get(subdomain);
|
|
53
|
+
res = clientData.get(`${subdomain}/${pathname}`);
|
|
51
54
|
|
|
52
55
|
if (retries < 1) {
|
|
53
|
-
|
|
54
|
-
return new Response("client not responding :(", { status: 500 });
|
|
56
|
+
return new Response("client not responding", { status: 500 });
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
const { status, statusText, headers: resHeaders, body } = JSON.parse(res);
|
|
61
|
+
const init = { headers: resHeaders, status, statusText };
|
|
62
|
+
delete resHeaders["content-encoding"];
|
|
63
|
+
delete resHeaders["Content-Encoding"];
|
|
64
|
+
|
|
65
|
+
return new Response(body, init);
|
|
59
66
|
},
|
|
60
67
|
websocket: {
|
|
61
68
|
open(ws) {
|
|
62
|
-
console.log("connecting to", ws.data.id);
|
|
63
69
|
clients.set(ws.data.id, ws);
|
|
70
|
+
console.log(
|
|
71
|
+
`\x1b[32mconnected to ${ws.data.id} (${clients.size} total)\x1b[0m`
|
|
72
|
+
);
|
|
64
73
|
ws.send(
|
|
65
74
|
JSON.stringify({
|
|
66
75
|
url: `${scheme}://${ws.data.id}.${domain}`,
|
|
@@ -69,7 +78,9 @@ serve<Client>({
|
|
|
69
78
|
},
|
|
70
79
|
message(ws, message) {
|
|
71
80
|
console.log("message from", ws.data.id);
|
|
72
|
-
|
|
81
|
+
|
|
82
|
+
const { pathname } = JSON.parse(message as string);
|
|
83
|
+
clientData.set(`${ws.data.id}/${pathname}`, message);
|
|
73
84
|
},
|
|
74
85
|
close(ws) {
|
|
75
86
|
console.log("closing", ws.data.id);
|