commit 3d630ed1f233a49ddcac99ab3b668ad2e9c2ba55 Author: Chris Wanstrath Date: Tue Sep 23 19:11:39 2025 -0700 sneaker diff --git a/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 120000 index 0000000..6100270 --- /dev/null +++ b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1 @@ +../../CLAUDE.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b8100b7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; + +// import .css files directly and it works +import './index.css'; + +import { createRoot } from "react-dom/client"; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d9b10d --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 👟 sneaker + +don't ask \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f5ca7ce --- /dev/null +++ b/bun.lock @@ -0,0 +1,37 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "sneaker", + "dependencies": { + "hono": "^4.9.8", + "unique-names-generator": "^4.7.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + + "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + + "unique-names-generator": ["unique-names-generator@4.7.1", "", {}, "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c61d658 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "sneaker", + "module": "src/server.tsx", + "type": "module", + "private": true, + "scripts": { + "dev": "bun --hot src/server.tsx", + "start": "bun run src/server.tsx" + }, + "dependencies": { + "hono": "^4.9.8", + "unique-names-generator": "^4.7.1" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/src/server.tsx b/src/server.tsx new file mode 100644 index 0000000..a9032d1 --- /dev/null +++ b/src/server.tsx @@ -0,0 +1,115 @@ +import { Hono } from "hono" +import { upgradeWebSocket, websocket } from "hono/bun" +import { uniqueNamesGenerator, adjectives, animals, colors } from "unique-names-generator" + +type Request = { + id: string + app: string + method: string + path: string + headers: Record, + body: string +} + +type Response = { + id: string + status: number + headers: Record, + body: string +} + +type Connection = { app: string, ws: any } +let connections: Record = {} +const pending = new Map void> + +function send(connection: any, msg: Request) { + console.log("sending", msg) + connection.send(JSON.stringify(msg)) +} + +const app = new Hono + +app.get("/tunnel", c => { + const app = c.req.query("app") + if (!app) { + return c.text("need ?app name", 502) + } + + return upgradeWebSocket(c, { + async onOpen(_event, ws) { + const name = randomName() + connections[name] = { app, ws } + console.log(`connection opened: ${name} -> ${app}`) + }, + onClose: (_event, ws) => { + for (const name of Object.keys(connections)) + if (connections[name]?.ws === ws) { + console.log("connection closed:", name) + delete connections[name] + break + } + }, + async onMessage(event, _ws) { + const msg = JSON.parse(event.data.toString()) + const resolve = pending.get(msg.id) + if (resolve) { + resolve(msg) + pending.delete(msg.id) + } + }, + }) +}) + +app.get("*", async c => { + const url = new URL(c.req.url) + const localhost = url.hostname.endsWith("localhost") + const domains = url.hostname.split(".") + let subdomain = "" + + if (domains.length > (localhost ? 1 : 2)) + subdomain = domains[0]! + + const connection = connections[subdomain] + if (!connection) + return c.text("No tunnel", 502) + + const id = randomID() + const headers = Object.fromEntries(c.req.raw.headers) + const body = await c.req.text() + const app = connection.app + + const result = await new Promise(resolve => { + pending.set(id, resolve) + send(connection.ws, { + id, + app, + method: c.req.method, + path: c.req.path, + headers, + body + }) + }) + + return new Response(result.body, { + status: result.status, + headers: result.headers, + }) +}) + +function randomID(): string { + return Math.random().toString(36).slice(2, 10) +} + +function randomName(): string { + return uniqueNamesGenerator({ + dictionaries: [adjectives, animals], + separator: "-", + style: "lowerCase", + }) +} + +export default { + port: process.env.PORT || 3100, + websocket, + fetch: app.fetch, +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..42edcc8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "baseUrl": "." + } +}