Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
401d774be2 Fix test mutations and improve assertions
Changes:
- Use [...routes].sort() instead of routes.sort() to avoid mutating the
  returned array from the getter (immutability fix)
- Improve long path test to assert the actual expected path string
  instead of just checking lengths, providing better regression detection
- Remove noisy console.log statements in favor of JSON output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 09:49:16 +00:00
Claude Bot
7b464937c3 Add wildcard route test
Tests that wildcard routes ("/*") are correctly included in the routes
array alongside specific routes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 09:41:27 +00:00
Claude Bot
51e617f7f4 Deduplicate route paths in server.routes getter
Routes with multiple HTTP methods (GET, POST, etc.) previously appeared
multiple times in the routes array. Now each route path appears only once,
making the API more intuitive.

Changes:
- Use StringHashMap to collect unique paths before creating the array
- Updated tests to verify deduplication with exact length checks
- Added new edge case tests:
  - Immediate route access after server creation
  - Very long route paths
  - Explicit deduplication verification with multiple HTTP methods

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 09:33:28 +00:00
Claude Bot
5c5f32faff Add routes getter to Bun.serve() to list all active routes
Implements a new `routes` getter property on server objects that returns an Array<string> containing all route paths that are currently registered with the server.

This works with:
- Static route paths (e.g., "/api/users")
- Parameterized routes (e.g., "/users/:id")
- HTTP method-specific routes (each method creates a separate entry)

The getter uses constructEmptyArray and createUTF8ForJS as requested to efficiently create and populate the JavaScript array from the Zig implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 08:54:53 +00:00
3 changed files with 440 additions and 0 deletions

View File

@@ -82,6 +82,9 @@ function generate(name) {
development: {
getter: "getDevelopment",
},
routes: {
getter: "getRoutes",
},
},
klass: {},
finalize: true,

View File

@@ -1423,6 +1423,31 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
return jsc.JSValue.jsBoolean(debug_mode);
}
pub fn getRoutes(this: *ThisServer, globalThis: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue {
const routes = this.user_routes.items;
// Collect unique paths using a StringHashMap
var seen_paths = std.StringHashMap(void).init(bun.default_allocator);
defer seen_paths.deinit();
for (routes) |route| {
const path = route.route.path;
try seen_paths.put(path, {});
}
// Create array with unique paths
const array = try jsc.JSValue.createEmptyArray(globalThis, seen_paths.count());
var i: u32 = 0;
var iter = seen_paths.keyIterator();
while (iter.next()) |path| {
const path_str = try bun.String.createUTF8ForJS(globalThis, path.*);
try array.putIndex(globalThis, i, path_str);
i += 1;
}
return array;
}
pub fn onStaticRequestComplete(this: *ThisServer) void {
this.pending_requests -= 1;
this.deinitIfWeCan();

View File

@@ -0,0 +1,412 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
describe("server.routes getter", () => {
test("should return empty array when no routes are defined", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
fetch(req) {
return new Response("Hello");
}
});
console.log(JSON.stringify(server.routes));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("[]");
expect(exitCode).toBe(0);
});
test("should return array with static route paths", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/api/users": () => new Response("users"),
"/api/posts": () => new Response("posts"),
"/api/comments": () => new Response("comments"),
},
fetch(req) {
return new Response("fallback");
}
});
const routes = server.routes;
console.log(JSON.stringify([...routes].sort()));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const routes = JSON.parse(stdout.trim());
expect(routes).toEqual(["/api/comments", "/api/posts", "/api/users"]);
expect(exitCode).toBe(0);
});
test("should work with parameterized routes", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/users/:id": (req) => new Response("user"),
"/posts/:postId/comments/:commentId": (req) => new Response("comment"),
},
fetch(req) {
return new Response("fallback");
}
});
const routes = server.routes;
console.log(JSON.stringify([...routes].sort()));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const routes = JSON.parse(stdout.trim());
expect(routes).toEqual(["/posts/:postId/comments/:commentId", "/users/:id"]);
expect(exitCode).toBe(0);
});
test("should work with HTTP method specific routes", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/api": {
GET: () => new Response("GET"),
POST: () => new Response("POST"),
PUT: () => new Response("PUT"),
},
"/users/:id": {
GET: (req) => new Response("get user"),
DELETE: (req) => new Response("delete user"),
}
},
fetch(req) {
return new Response("fallback");
}
});
const routes = server.routes;
console.log(JSON.stringify([...routes].sort()));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const routes = JSON.parse(stdout.trim());
// Should deduplicate paths - each path should appear only once
expect(routes.length).toBe(2);
expect(new Set(routes).size).toBe(2);
expect(routes).toEqual(["/api", "/users/:id"]);
expect(exitCode).toBe(0);
});
test("should work with mixed static and function routes", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/static": () => new Response("static"),
"/another": () => new Response("another"),
},
fetch(req) {
return new Response("fallback");
}
});
const routes = server.routes;
console.log(JSON.stringify([...routes].sort()));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const routes = JSON.parse(stdout.trim());
expect(routes).toEqual(["/another", "/static"]);
expect(exitCode).toBe(0);
});
test("should return array that can be iterated", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/api/a": () => new Response("a"),
"/api/b": () => new Response("b"),
"/api/c": () => new Response("c"),
},
fetch(req) {
return new Response("fallback");
}
});
const routes = server.routes;
console.log(Array.isArray(routes));
console.log(routes.length);
console.log(typeof routes[0]);
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe("true");
expect(lines[1]).toBe("3");
expect(lines[2]).toBe("string");
expect(exitCode).toBe(0);
});
test("should work after server reload", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/before": () => new Response("before"),
},
fetch(req) {
return new Response("fallback");
}
});
console.log(JSON.stringify([...server.routes].sort()));
server.reload({
routes: {
"/after": () => new Response("after"),
},
fetch(req) {
return new Response("fallback");
}
});
console.log(JSON.stringify([...server.routes].sort()));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const lines = stdout.trim().split("\n");
expect(JSON.parse(lines[0])).toEqual(["/before"]);
expect(JSON.parse(lines[1])).toEqual(["/after"]);
expect(exitCode).toBe(0);
});
test("should return routes immediately after creation", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/immediate": () => new Response("test"),
"/another": () => new Response("test2"),
},
fetch(req) {
return new Response("fallback");
}
});
// Check routes immediately without making any requests
console.log(JSON.stringify([...server.routes].sort()));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const routes = JSON.parse(stdout.trim());
expect(routes).toEqual(["/another", "/immediate"]);
expect(exitCode).toBe(0);
});
test("should handle very long route paths", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const longPath = "/" + "segment/".repeat(50) + "end";
const server = Bun.serve({
port: 0,
routes: {
[longPath]: () => new Response("long"),
"/short": () => new Response("short"),
},
fetch(req) {
return new Response("fallback");
}
});
const routes = server.routes;
console.log(JSON.stringify(routes.sort()));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const routes = JSON.parse(stdout.trim());
const expectedLongPath = "/" + "segment/".repeat(50) + "end";
expect(routes.length).toBe(2);
expect(routes).toContain("/short");
expect(routes).toContain(expectedLongPath);
expect(routes.find(r => r.length > 400)).toBe(expectedLongPath);
expect(exitCode).toBe(0);
});
test("should handle duplicate deduplication correctly", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/duplicate": {
GET: () => new Response("GET"),
POST: () => new Response("POST"),
PUT: () => new Response("PUT"),
DELETE: () => new Response("DELETE"),
PATCH: () => new Response("PATCH"),
}
},
fetch(req) {
return new Response("fallback");
}
});
const routes = server.routes;
console.log(routes.length);
console.log(new Set(routes).size);
console.log(JSON.stringify(routes));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe("1"); // Only 1 unique route
expect(lines[1]).toBe("1"); // Set size confirms uniqueness
expect(JSON.parse(lines[2])).toEqual(["/duplicate"]);
expect(exitCode).toBe(0);
});
test("should include wildcard routes", async () => {
using dir = tempDir("serve-routes-test", {
"server.js": `
const server = Bun.serve({
port: 0,
routes: {
"/*": () => new Response("wildcard"),
"/specific": () => new Response("specific"),
},
fetch(req) {
return new Response("fallback");
}
});
console.log(JSON.stringify([...server.routes].sort()));
server.stop();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const routes = JSON.parse(stdout.trim());
expect(routes).toContain("/*");
expect(routes).toContain("/specific");
expect(routes.length).toBe(2);
expect(exitCode).toBe(0);
});
});