@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 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 localhost:1234 --subdomain my-subdomain
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 serverUrl = `ws://${domain || "localhost:1234"}?new${
13
- subdomain ? `&subdomain=${subdomain}` : ""
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.path}`, {
29
+ const res = await fetch(`${url}${data.pathname}`, {
23
30
  method: data.method,
24
31
  headers: data.headers,
25
- })
26
- .then((res) => res.text())
27
- .then((res) => socket.send(res));
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 tunnel.example.so -s my-subdomain`
38
- * > my-subdomain.tunnel.example.so will be proxied to localhost:3000
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:${values.port}`,
64
- domain: values.domain,
65
- subdomain: values.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": "expose localhost to the world",
7
- "version": "0.0.11",
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
- const id = requested && !clients.has(requested) ? requested : uid();
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 path = new URL(url).pathname;
36
- client.send(JSON.stringify({ method, path, headers }));
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
- console.log(`\x1b[31m${subdomain} not responding \x1b[0m`);
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
- return new Response(res);
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
- clientData.set(ws.data.id, message);
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);