Skip to content

Commit a1dc86c

Browse files
committed
Add listen function
1 parent 716949b commit a1dc86c

File tree

13 files changed

+420
-79
lines changed

13 files changed

+420
-79
lines changed

README.md

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,15 @@ app.use(async (ctx, next) => {
4343
});
4444

4545
// Routes
46-
app.get('/', (ctx) => ctx.text('Hello Bun! 🐇'));
46+
app.get('/', (ctx) => ctx.text('Hello!'));
4747

4848
app.get('/users/:id', async (ctx) => {
4949
const user = await getUser(ctx.params.id);
5050
return ctx.json(user);
5151
});
5252

53-
// Start with Bun server
54-
Bun.serve({
55-
port: 3000,
56-
fetch: app.handleBun
57-
});
58-
59-
// Start with Deno server
60-
Deno.serve({ port: 3000 },
61-
(req, info) => app.handleDeno(req, info)
62-
);
63-
64-
// Start with Node server
65-
import { createServer } from "http";
66-
createServer((req, res) => app.handleNode(req, res)).listen(3000);
53+
// Start server
54+
app.listen({ port: 3000 });
6755

6856
console.log('Server running at http://localhost:3000');
6957
```

examples/bun.ts

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -543,39 +543,12 @@ app.onError((err, ctx) => {
543543

544544
// ===== SERVER CONFIGURATION =====
545545

546-
const PORT = parseInt(process.env.PORT || "8080");
546+
const PORT = parseInt(process.env.PORT || "3000");
547547
const HOSTNAME = "0.0.0.0";
548548

549-
// Create Bun server with WebSocket support
550-
const server = Bun.serve({
551-
port: PORT,
549+
const server = await app.listen({
552550
hostname: HOSTNAME,
553-
554-
fetch: app.handleBun,
555-
556-
// WebSocket configuration
557-
websocket: {
558-
open(ws) {
559-
console.log("WebSocket opened");
560-
ws.send(JSON.stringify({ type: "welcome", message: "Connected to WebSocket" }));
561-
},
562-
563-
message(ws, message) {
564-
console.log("WebSocket message:", message);
565-
// Echo the message back
566-
ws.send(JSON.stringify({ type: "echo", data: message }));
567-
},
568-
569-
close(ws) {
570-
console.log("WebSocket closed");
571-
},
572-
},
573-
574-
// Server error handler
575-
error(error) {
576-
console.error("Server error:", error);
577-
return new Response("Internal Server Error", { status: 500 });
578-
},
551+
port: PORT,
579552
});
580553

581554
// Graceful shutdown

examples/deno.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,4 @@ app.use(cors());
77

88
app.get("/", async (ctx) => ctx.html(`<h1>Hello World from ${ctx.clientIp}</h1>`));
99

10-
Deno.serve(
11-
{
12-
port: 8080,
13-
},
14-
(req, info) => app.handleDeno(req, info)
15-
);
10+
app.listen({ port: 3000 });

examples/node.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { createServer } from "http";
21
import { Web } from "../packages/core/dist/index.js";
32

43
const app = new Web();
54

65
app.get("/", async (ctx) => ctx.html(`<h1>Hello World from ${ctx.clientIp}</h1>`));
76

8-
createServer((req, res) => app.handleNode(req, res)).listen(3000);
7+
app.listen({ port: 3000 });

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web-monorepo",
3-
"version": "0.4.0",
3+
"version": "0.7.0",
44
"description": "High-performance web framework monorepo",
55
"private": true,
66
"type": "module",

packages/core/README.md

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,15 @@ app.use(async (ctx, next) => {
4343
});
4444

4545
// Routes
46-
app.get('/', (ctx) => ctx.text('Hello Bun! 🐇'));
46+
app.get('/', (ctx) => ctx.text('Hello!'));
4747

4848
app.get('/users/:id', async (ctx) => {
4949
const user = await getUser(ctx.params.id);
5050
return ctx.json(user);
5151
});
5252

53-
// Start with Bun server
54-
Bun.serve({
55-
port: 3000,
56-
fetch: app.handleBun
57-
});
58-
59-
// Start with Deno server
60-
Deno.serve({ port: 3000 },
61-
(req, info) => app.handleDeno(req, info)
62-
);
63-
64-
// Start with Node server
65-
import { createServer } from "http";
66-
createServer((req, res) => app.handleNode(req, res)).listen(3000);
53+
// Start server
54+
app.listen({ port: 3000 });
6755

6856
console.log('Server running at http://localhost:3000');
6957
```

packages/core/jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"license": "MIT",
55
"exports": "./src/index.ts",
66
"publish": {

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "High-performance web framework",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

packages/core/src/index.ts

Lines changed: 231 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,34 @@
1-
import type { Context, MatchResult, Method, Middleware, MiddlewareRoute, Next, Route } from "./types";
1+
import type {
2+
BunServerInstance,
3+
Context,
4+
DenoServerInstance,
5+
ListenOptions,
6+
MatchResult,
7+
Method,
8+
Middleware,
9+
MiddlewareRoute,
10+
Next,
11+
NodeServerInstance,
12+
Route,
13+
Server,
14+
} from "./types";
15+
16+
/**
17+
* Runtime detection utilities for identifying the current JavaScript runtime environment.
18+
* @internal
19+
*/
20+
const Runtime = {
21+
/** True if running in Bun runtime */
22+
isBun: typeof globalThis !== "undefined" && typeof (globalThis as any).Bun !== "undefined",
23+
/** True if running in Deno runtime */
24+
isDeno: typeof globalThis !== "undefined" && typeof (globalThis as any).Deno !== "undefined",
25+
/** True if running in Node.js runtime */
26+
isNode:
27+
typeof globalThis !== "undefined" &&
28+
typeof (globalThis as any).process !== "undefined" &&
29+
typeof (globalThis as any).Bun === "undefined" &&
30+
typeof (globalThis as any).Deno === "undefined",
31+
};
232

333
/**
434
* A node in the trie data structure used for efficient route matching.
@@ -1711,7 +1741,206 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>> {
17111741
res.writeHead(500, { "Content-Type": "text/plain" });
17121742
}
17131743
res.end("Internal Server Error");
1714-
console.error("Error in handleNode:", error);
1744+
}
1745+
}
1746+
1747+
/**
1748+
* Starts a server with automatic runtime detection.
1749+
* Automatically selects the appropriate server implementation based on the runtime environment.
1750+
*
1751+
* @param {ListenOptions} options - Server configuration options
1752+
* @returns {Promise<Server>} Promise that resolves to a Server instance
1753+
* @throws {Error} If the runtime is not supported
1754+
*
1755+
* @example
1756+
* ```typescript
1757+
* // Basic usage with default options
1758+
* const server = await app.listen({ port: 3000 });
1759+
* console.log(`Server running on ${server.runtime} at http://localhost:${server.port}`);
1760+
*
1761+
* // With startup callback
1762+
* await app.listen({
1763+
* port: 8080,
1764+
* hostname: '0.0.0.0',
1765+
* onListen: ({ port, hostname, runtime }) => {
1766+
* console.log(`✅ ${runtime} server running at http://${hostname}:${port}`);
1767+
* }
1768+
* });
1769+
*
1770+
* // With HTTPS in Node.js
1771+
* await app.listen({
1772+
* port: 443,
1773+
* node: {
1774+
* https: true,
1775+
* key: fs.readFileSync('./key.pem'),
1776+
* cert: fs.readFileSync('./cert.pem')
1777+
* }
1778+
* });
1779+
*
1780+
* // With TLS in Deno
1781+
* await app.listen({
1782+
* port: 443,
1783+
* deno: {
1784+
* key: './key.pem',
1785+
* cert: './cert.pem'
1786+
* }
1787+
* });
1788+
*
1789+
* // With TLS in Bun
1790+
* await app.listen({
1791+
* port: 443,
1792+
* bun: {
1793+
* tls: {
1794+
* key: Bun.file('./key.pem'),
1795+
* cert: Bun.file('./cert.pem')
1796+
* }
1797+
* }
1798+
* });
1799+
*
1800+
* // Gracefully stop the server
1801+
* await server.stop();
1802+
* ```
1803+
*/
1804+
async listen(options: ListenOptions = {}): Promise<Server> {
1805+
const { port = 3000, hostname = "localhost", onListen, node: nodeOptions = {}, deno: denoOptions = {}, bun: bunOptions = {} } = options;
1806+
1807+
// Detect runtime and start appropriate server
1808+
if (Runtime.isBun) {
1809+
// Bun runtime
1810+
const bunGlobal = globalThis as any;
1811+
const serverConfig: Record<string, any> = {
1812+
port,
1813+
hostname,
1814+
fetch: this.handleBun.bind(this),
1815+
...bunOptions,
1816+
};
1817+
1818+
const bunServer = bunGlobal.Bun.serve(serverConfig) as BunServerInstance;
1819+
1820+
const server: Server = {
1821+
port: bunServer.port,
1822+
hostname: bunServer.hostname || hostname,
1823+
runtime: "bun",
1824+
instance: bunServer,
1825+
stop: async (): Promise<void> => {
1826+
bunServer.stop();
1827+
},
1828+
};
1829+
1830+
if (onListen) {
1831+
onListen({
1832+
port: server.port,
1833+
hostname: server.hostname,
1834+
runtime: server.runtime,
1835+
});
1836+
}
1837+
1838+
return server;
1839+
} else if (Runtime.isDeno) {
1840+
// Deno runtime
1841+
const denoGlobal = globalThis as any;
1842+
const serverConfig: Record<string, any> = {
1843+
port,
1844+
hostname,
1845+
...denoOptions,
1846+
};
1847+
1848+
const handler = (req: Request, info: unknown): Promise<Response> => {
1849+
return this.handleDeno(req, info);
1850+
};
1851+
1852+
const denoServer = denoGlobal.Deno.serve(serverConfig, handler) as DenoServerInstance;
1853+
1854+
const server: Server = {
1855+
port,
1856+
hostname,
1857+
runtime: "deno",
1858+
instance: denoServer,
1859+
stop: async (): Promise<void> => {
1860+
await denoServer.shutdown();
1861+
},
1862+
};
1863+
1864+
if (onListen) {
1865+
onListen({
1866+
port: server.port,
1867+
hostname: server.hostname,
1868+
runtime: server.runtime,
1869+
});
1870+
}
1871+
1872+
return server;
1873+
} else if (Runtime.isNode) {
1874+
// Node.js runtime
1875+
const module: "http" | "https" = nodeOptions.https ? "https" : "http";
1876+
const { createServer } = await import(module);
1877+
1878+
let nodeServer: NodeServerInstance;
1879+
1880+
const requestHandler = (req: any, res: any): void => {
1881+
this.handleNode(req, res).catch((err: Error) => {
1882+
if (!res.headersSent) {
1883+
res.writeHead(500, { "Content-Type": "text/plain" });
1884+
res.end("Internal Server Error");
1885+
}
1886+
});
1887+
};
1888+
1889+
if (nodeOptions.https) {
1890+
// HTTPS server
1891+
const httpsOptions: Record<string, any> = {
1892+
key: nodeOptions.key,
1893+
cert: nodeOptions.cert,
1894+
};
1895+
nodeServer = (createServer as any)(httpsOptions, requestHandler) as NodeServerInstance;
1896+
} else {
1897+
// HTTP server
1898+
nodeServer = createServer(requestHandler) as NodeServerInstance;
1899+
}
1900+
1901+
// Start listening
1902+
await new Promise<void>((resolve, reject) => {
1903+
const errorHandler = (err: Error): void => {
1904+
reject(err);
1905+
};
1906+
1907+
nodeServer.on("error", errorHandler);
1908+
1909+
nodeServer.listen(port, hostname, () => {
1910+
nodeServer.off("error", errorHandler);
1911+
resolve();
1912+
});
1913+
});
1914+
1915+
const server: Server = {
1916+
port,
1917+
hostname,
1918+
runtime: "node",
1919+
instance: nodeServer,
1920+
stop: async (): Promise<void> => {
1921+
return new Promise<void>((resolve, reject) => {
1922+
nodeServer.close((err?: Error) => {
1923+
if (err) {
1924+
reject(err);
1925+
} else {
1926+
resolve();
1927+
}
1928+
});
1929+
});
1930+
},
1931+
};
1932+
1933+
if (onListen) {
1934+
onListen({
1935+
port: server.port,
1936+
hostname: server.hostname,
1937+
runtime: server.runtime,
1938+
});
1939+
}
1940+
1941+
return server;
1942+
} else {
1943+
throw new Error(`Unsupported runtime. This framework supports Bun, Deno, and Node.js.`);
17151944
}
17161945
}
17171946
}

0 commit comments

Comments
 (0)