mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 15:38:46 +00:00
Compare commits
9 Commits
ciro/fix-a
...
claude/dir
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1f95ab7bf | ||
|
|
9f9566c279 | ||
|
|
5384862b5b | ||
|
|
5cbcd05c45 | ||
|
|
fb9e1af73d | ||
|
|
e72e94a92e | ||
|
|
2003f97f03 | ||
|
|
3a4ec715be | ||
|
|
a210ef439e |
@@ -79,6 +79,7 @@ src/bun.js/api/JSBundler.zig
|
||||
src/bun.js/api/JSTranspiler.zig
|
||||
src/bun.js/api/server.zig
|
||||
src/bun.js/api/server/AnyRequestContext.zig
|
||||
src/bun.js/api/server/DirectoryRoute.zig
|
||||
src/bun.js/api/server/FileRoute.zig
|
||||
src/bun.js/api/server/HTMLBundle.zig
|
||||
src/bun.js/api/server/HTTPStatusText.zig
|
||||
|
||||
@@ -62,6 +62,7 @@ pub fn writeStatus(comptime ssl: bool, resp_ptr: ?*uws.NewApp(ssl).Response, sta
|
||||
// TODO: rename to StaticBlobRoute? the html bundle is sometimes a static route
|
||||
pub const StaticRoute = @import("./server/StaticRoute.zig");
|
||||
pub const FileRoute = @import("./server/FileRoute.zig");
|
||||
pub const DirectoryRoute = @import("./server/DirectoryRoute.zig");
|
||||
|
||||
const HTMLBundle = JSC.API.HTMLBundle;
|
||||
|
||||
@@ -71,6 +72,9 @@ pub const AnyRoute = union(enum) {
|
||||
static: *StaticRoute,
|
||||
/// Serve a file from disk
|
||||
file: *FileRoute,
|
||||
/// Serve files from a directory
|
||||
/// "/assets": { dir: "./public" },
|
||||
directory: *DirectoryRoute,
|
||||
/// Bundle an HTML import
|
||||
/// import html from "./index.html";
|
||||
/// "/": html,
|
||||
@@ -86,6 +90,7 @@ pub const AnyRoute = union(enum) {
|
||||
return switch (this) {
|
||||
.static => |static_route| static_route.memoryCost(),
|
||||
.file => |file_route| file_route.memoryCost(),
|
||||
.directory => |directory_route| directory_route.memoryCost(),
|
||||
.html => |html_bundle_route| html_bundle_route.data.memoryCost(),
|
||||
.framework_router => @sizeOf(bun.bake.Framework.FileSystemRouterType),
|
||||
};
|
||||
@@ -95,6 +100,7 @@ pub const AnyRoute = union(enum) {
|
||||
switch (this) {
|
||||
.static => |static_route| static_route.server = server,
|
||||
.file => |file_route| file_route.server = server,
|
||||
.directory => |directory_route| directory_route.server = server,
|
||||
.html => |html_bundle_route| html_bundle_route.server = server,
|
||||
.framework_router => {}, // DevServer contains .server field
|
||||
}
|
||||
@@ -104,6 +110,7 @@ pub const AnyRoute = union(enum) {
|
||||
switch (this) {
|
||||
.static => |static_route| static_route.deref(),
|
||||
.file => |file_route| file_route.deref(),
|
||||
.directory => |directory_route| directory_route.deref(),
|
||||
.html => |html_bundle_route| html_bundle_route.deref(),
|
||||
.framework_router => {}, // not reference counted
|
||||
}
|
||||
@@ -113,6 +120,7 @@ pub const AnyRoute = union(enum) {
|
||||
switch (this) {
|
||||
.static => |static_route| static_route.ref(),
|
||||
.file => |file_route| file_route.ref(),
|
||||
.directory => |directory_route| directory_route.ref(),
|
||||
.html => |html_bundle_route| html_bundle_route.ref(),
|
||||
.framework_router => {}, // not reference counted
|
||||
}
|
||||
@@ -270,42 +278,52 @@ pub const AnyRoute = union(enum) {
|
||||
}
|
||||
|
||||
if (argument.isObject()) {
|
||||
const FrameworkRouter = bun.bake.FrameworkRouter;
|
||||
if (try argument.getOptional(global, "dir", bun.String.Slice)) |dir| {
|
||||
var alloc = init_ctx.js_string_allocations;
|
||||
const relative_root = alloc.track(dir);
|
||||
if (try argument.get(global, "dir")) |dir_value| {
|
||||
const dir_slice = try dir_value.toSlice(global, bun.default_allocator);
|
||||
defer dir_slice.deinit();
|
||||
|
||||
// Check if this is a framework router (has a "style" property) or a simple directory route
|
||||
if (try argument.get(global, "style")) |style| {
|
||||
// Framework router
|
||||
const FrameworkRouter = bun.bake.FrameworkRouter;
|
||||
var alloc = init_ctx.js_string_allocations;
|
||||
const relative_root = alloc.track(dir_slice);
|
||||
|
||||
var style: FrameworkRouter.Style = if (try argument.get(global, "style")) |style|
|
||||
try FrameworkRouter.Style.fromJS(style, global)
|
||||
else
|
||||
.nextjs_pages;
|
||||
errdefer style.deinit();
|
||||
var style_parsed: FrameworkRouter.Style = try FrameworkRouter.Style.fromJS(style, global);
|
||||
errdefer style_parsed.deinit();
|
||||
|
||||
if (!bun.strings.endsWith(path, "/*")) {
|
||||
return global.throwInvalidArguments("To mount a directory, make sure the path ends in `/*`", .{});
|
||||
if (!bun.strings.endsWith(path, "/*")) {
|
||||
return global.throwInvalidArguments("To mount a directory, make sure the path ends in `/*`", .{});
|
||||
}
|
||||
|
||||
try init_ctx.framework_router_list.append(.{
|
||||
.root = relative_root,
|
||||
.style = style_parsed,
|
||||
|
||||
// trim the /*
|
||||
.prefix = if (path.len == 2) "/" else path[0 .. path.len - 2],
|
||||
|
||||
// TODO: customizable framework option.
|
||||
.entry_client = "bun-framework-react/client.tsx",
|
||||
.entry_server = "bun-framework-react/server.tsx",
|
||||
.ignore_underscores = true,
|
||||
.ignore_dirs = &.{ "node_modules", ".git" },
|
||||
.extensions = &.{ ".tsx", ".jsx" },
|
||||
.allow_layouts = true,
|
||||
});
|
||||
|
||||
const limit = std.math.maxInt(@typeInfo(FrameworkRouter.Type.Index).@"enum".tag_type);
|
||||
if (init_ctx.framework_router_list.items.len > limit) {
|
||||
return global.throwInvalidArguments("Too many framework routers. Maximum is {d}.", .{limit});
|
||||
}
|
||||
return .{ .framework_router = .init(@intCast(init_ctx.framework_router_list.items.len - 1)) };
|
||||
} else {
|
||||
// Simple directory route
|
||||
const directory_route = DirectoryRoute.init(dir_slice.slice()) catch |err| {
|
||||
return global.throwInvalidArguments("Failed to open directory {s}: {s}", .{ dir_slice.slice(), @errorName(err) });
|
||||
};
|
||||
return .{ .directory = directory_route };
|
||||
}
|
||||
|
||||
try init_ctx.framework_router_list.append(.{
|
||||
.root = relative_root,
|
||||
.style = style,
|
||||
|
||||
// trim the /*
|
||||
.prefix = if (path.len == 2) "/" else path[0 .. path.len - 2],
|
||||
|
||||
// TODO: customizable framework option.
|
||||
.entry_client = "bun-framework-react/client.tsx",
|
||||
.entry_server = "bun-framework-react/server.tsx",
|
||||
.ignore_underscores = true,
|
||||
.ignore_dirs = &.{ "node_modules", ".git" },
|
||||
.extensions = &.{ ".tsx", ".jsx" },
|
||||
.allow_layouts = true,
|
||||
});
|
||||
|
||||
const limit = std.math.maxInt(@typeInfo(FrameworkRouter.Type.Index).@"enum".tag_type);
|
||||
if (init_ctx.framework_router_list.items.len > limit) {
|
||||
return global.throwInvalidArguments("Too many framework routers. Maximum is {d}.", .{limit});
|
||||
}
|
||||
return .{ .framework_router = .init(@intCast(init_ctx.framework_router_list.items.len - 1)) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2627,6 +2645,20 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
.file => |file_route| {
|
||||
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *FileRoute, file_route, entry.path, entry.method);
|
||||
},
|
||||
.directory => |directory_route| {
|
||||
// Directory routes need to handle the exact path AND all sub-paths
|
||||
if (std.mem.eql(u8, entry.path, "/")) {
|
||||
// For root path, register "*" first then "/" to handle all paths and root
|
||||
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *DirectoryRoute, directory_route, "*", entry.method);
|
||||
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *DirectoryRoute, directory_route, "/", entry.method);
|
||||
} else {
|
||||
// For other paths, register wildcard first then exact path
|
||||
const dir_path = std.fmt.allocPrint(bun.default_allocator, "{s}/*", .{entry.path}) catch bun.outOfMemory();
|
||||
defer bun.default_allocator.free(dir_path);
|
||||
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *DirectoryRoute, directory_route, dir_path, entry.method);
|
||||
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *DirectoryRoute, directory_route, entry.path, entry.method);
|
||||
}
|
||||
},
|
||||
.html => |html_bundle_route| {
|
||||
ServerConfig.applyStaticRoute(any_server, ssl_enabled, app, *HTMLBundle.Route, html_bundle_route.data, entry.path, entry.method);
|
||||
if (dev_server) |dev| {
|
||||
|
||||
287
src/bun.js/api/server/DirectoryRoute.zig
Normal file
287
src/bun.js/api/server/DirectoryRoute.zig
Normal file
@@ -0,0 +1,287 @@
|
||||
const DirectoryRoute = @This();
|
||||
|
||||
ref_count: RefCount,
|
||||
server: ?AnyServer = null,
|
||||
directory_path: []const u8,
|
||||
directory_fd: bun.FileDescriptor,
|
||||
|
||||
pub fn init(directory_path: []const u8) !*DirectoryRoute {
|
||||
const path_duped = bun.default_allocator.dupe(u8, directory_path) catch bun.outOfMemory();
|
||||
errdefer bun.default_allocator.free(path_duped);
|
||||
|
||||
const fd = switch (bun.sys.openA(path_duped, bun.O.DIRECTORY | bun.O.RDONLY | bun.O.CLOEXEC, 0)) {
|
||||
.result => |fd| fd,
|
||||
.err => {
|
||||
bun.default_allocator.free(path_duped);
|
||||
return error.AccessDenied;
|
||||
},
|
||||
};
|
||||
|
||||
return bun.new(DirectoryRoute, .{
|
||||
.ref_count = .init(),
|
||||
.directory_path = path_duped,
|
||||
.directory_fd = fd,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit(this: *DirectoryRoute) void {
|
||||
this.directory_fd.close();
|
||||
bun.default_allocator.free(this.directory_path);
|
||||
bun.destroy(this);
|
||||
}
|
||||
|
||||
pub fn memoryCost(this: *const DirectoryRoute) usize {
|
||||
return @sizeOf(DirectoryRoute) + this.directory_path.len;
|
||||
}
|
||||
|
||||
pub fn fromJS(globalThis: *JSC.JSGlobalObject, argument: JSC.JSValue) bun.JSError!?*DirectoryRoute {
|
||||
if (argument.isObject()) {
|
||||
if (try argument.get(globalThis, "dir")) |dir_value| {
|
||||
const dir_slice = try dir_value.toSlice(globalThis, bun.default_allocator);
|
||||
defer dir_slice.deinit();
|
||||
|
||||
return DirectoryRoute.init(dir_slice.slice()) catch |err| {
|
||||
return globalThis.throwInvalidArguments("Failed to open directory {s}: {s}", .{ dir_slice.slice(), @errorName(err) });
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn onHEADRequest(this: *DirectoryRoute, req: *uws.Request, resp: AnyResponse) void {
|
||||
this.on(req, resp, .HEAD);
|
||||
}
|
||||
|
||||
pub fn onRequest(this: *DirectoryRoute, req: *uws.Request, resp: AnyResponse) void {
|
||||
this.on(req, resp, bun.http.Method.find(req.method()) orelse .GET);
|
||||
}
|
||||
|
||||
pub fn on(this: *DirectoryRoute, req: *uws.Request, resp: AnyResponse, method: bun.http.Method) void {
|
||||
bun.debugAssert(this.server != null);
|
||||
this.ref();
|
||||
|
||||
if (this.server) |server| {
|
||||
server.onPendingRequest();
|
||||
resp.timeout(server.config().idleTimeout);
|
||||
}
|
||||
|
||||
const url = req.url();
|
||||
|
||||
// Always log for debugging
|
||||
std.debug.print("DirectoryRoute: URL={s}, directory={s}\n", .{ url, this.directory_path });
|
||||
|
||||
// Try to resolve the file path
|
||||
const file_path = this.resolveFilePath(url) catch {
|
||||
if (bun.Environment.enable_logs) {
|
||||
bun.Output.scoped(.DirectoryRoute, false)("Failed to resolve file path for URL: {s}", .{url});
|
||||
}
|
||||
req.setYield(true);
|
||||
this.deref();
|
||||
return;
|
||||
};
|
||||
defer bun.default_allocator.free(file_path);
|
||||
|
||||
if (bun.Environment.enable_logs) {
|
||||
bun.Output.scoped(.DirectoryRoute, false)("Resolved file path: {s}", .{file_path});
|
||||
}
|
||||
|
||||
// Try to open the file using openat
|
||||
const open_flags = bun.O.RDONLY | bun.O.CLOEXEC | bun.O.NONBLOCK;
|
||||
const fd_result = bun.sys.openatA(this.directory_fd, file_path, open_flags, 0);
|
||||
|
||||
if (fd_result == .err) {
|
||||
if (bun.Environment.enable_logs) {
|
||||
bun.Output.scoped(.DirectoryRoute, false)("Failed to open file: {s}, error: {s}", .{ file_path, @tagName(fd_result.err.getErrno()) });
|
||||
}
|
||||
|
||||
// Try with .html extension
|
||||
if (this.tryWithHtmlExtension(file_path)) |html_path| {
|
||||
defer bun.default_allocator.free(html_path);
|
||||
if (bun.Environment.enable_logs) {
|
||||
bun.Output.scoped(.DirectoryRoute, false)("Trying with .html extension: {s}", .{html_path});
|
||||
}
|
||||
const html_fd_result = bun.sys.openatA(this.directory_fd, html_path, open_flags, 0);
|
||||
|
||||
if (html_fd_result == .result) {
|
||||
if (bun.Environment.enable_logs) {
|
||||
bun.Output.scoped(.DirectoryRoute, false)("Found file with .html extension: {s}", .{html_path});
|
||||
}
|
||||
this.serveFile(req, resp, method, html_fd_result.result, html_path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try index.html or index.htm for directories
|
||||
if (this.tryIndexFiles(file_path)) |index_path| {
|
||||
defer bun.default_allocator.free(index_path);
|
||||
if (bun.Environment.enable_logs) {
|
||||
bun.Output.scoped(.DirectoryRoute, false)("Trying index file: {s}", .{index_path});
|
||||
}
|
||||
const index_fd_result = bun.sys.openatA(this.directory_fd, index_path, open_flags, 0);
|
||||
|
||||
if (index_fd_result == .result) {
|
||||
if (bun.Environment.enable_logs) {
|
||||
bun.Output.scoped(.DirectoryRoute, false)("Found index file: {s}", .{index_path});
|
||||
}
|
||||
this.serveFile(req, resp, method, index_fd_result.result, index_path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// File not found, yield to next handler
|
||||
if (bun.Environment.enable_logs) {
|
||||
bun.Output.scoped(.DirectoryRoute, false)("File not found, yielding to next handler", .{});
|
||||
}
|
||||
req.setYield(true);
|
||||
this.deref();
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = fd_result.result;
|
||||
|
||||
// Check if it's a directory
|
||||
const stat = switch (bun.sys.fstat(fd)) {
|
||||
.result => |s| s,
|
||||
.err => {
|
||||
bun.Async.Closer.close(fd, if (bun.Environment.isWindows) bun.windows.libuv.Loop.get());
|
||||
req.setYield(true);
|
||||
this.deref();
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
if (bun.S.ISDIR(@intCast(stat.mode))) {
|
||||
bun.Async.Closer.close(fd, if (bun.Environment.isWindows) bun.windows.libuv.Loop.get());
|
||||
|
||||
// Try index.html or index.htm for directories
|
||||
if (this.tryIndexFiles(file_path)) |index_path| {
|
||||
defer bun.default_allocator.free(index_path);
|
||||
const index_fd_result = bun.sys.openatA(this.directory_fd, index_path, open_flags, 0);
|
||||
|
||||
if (index_fd_result == .result) {
|
||||
this.serveFile(req, resp, method, index_fd_result.result, index_path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
req.setYield(true);
|
||||
this.deref();
|
||||
return;
|
||||
}
|
||||
|
||||
this.serveFile(req, resp, method, fd, file_path);
|
||||
}
|
||||
|
||||
fn resolveFilePath(this: *DirectoryRoute, url: []const u8) ![]const u8 {
|
||||
_ = this;
|
||||
|
||||
// Remove leading slash if present
|
||||
const clean_url = if (url.len > 0 and url[0] == '/') url[1..] else url;
|
||||
|
||||
// Basic path traversal protection - reject paths containing ".."
|
||||
if (std.mem.indexOf(u8, clean_url, "..") != null) {
|
||||
return error.InvalidPath;
|
||||
}
|
||||
|
||||
// If empty path, serve index
|
||||
if (clean_url.len == 0) {
|
||||
return bun.default_allocator.dupe(u8, ".");
|
||||
}
|
||||
|
||||
return bun.default_allocator.dupe(u8, clean_url);
|
||||
}
|
||||
|
||||
fn tryWithHtmlExtension(this: *DirectoryRoute, file_path: []const u8) ?[]const u8 {
|
||||
_ = this;
|
||||
|
||||
// Don't add .html if path already has an extension
|
||||
if (std.mem.lastIndexOfScalar(u8, file_path, '.') != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return std.fmt.allocPrint(bun.default_allocator, "{s}.html", .{file_path}) catch null;
|
||||
}
|
||||
|
||||
fn tryIndexFiles(this: *DirectoryRoute, file_path: []const u8) ?[]const u8 {
|
||||
_ = this;
|
||||
|
||||
// Don't add index.html if path already ends with it
|
||||
if (std.mem.endsWith(u8, file_path, "/index.html")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base_path = if (std.mem.eql(u8, file_path, ".")) "" else file_path;
|
||||
|
||||
// Try index.html first
|
||||
const index_html = if (base_path.len == 0)
|
||||
bun.default_allocator.dupe(u8, "index.html") catch null
|
||||
else
|
||||
std.fmt.allocPrint(bun.default_allocator, "{s}/index.html", .{base_path}) catch null;
|
||||
|
||||
if (index_html) |path| {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Try index.htm as fallback
|
||||
if (base_path.len == 0) {
|
||||
return bun.default_allocator.dupe(u8, "index.htm") catch null;
|
||||
} else {
|
||||
return std.fmt.allocPrint(bun.default_allocator, "{s}/index.htm", .{base_path}) catch null;
|
||||
}
|
||||
}
|
||||
|
||||
fn serveFile(this: *DirectoryRoute, req: *uws.Request, resp: AnyResponse, method: bun.http.Method, fd: bun.FileDescriptor, file_path: []const u8) void {
|
||||
// Close the file descriptor since we'll let FileRoute open it with a path
|
||||
bun.Async.Closer.close(fd, if (bun.Environment.isWindows) bun.windows.libuv.Loop.get());
|
||||
|
||||
// Create full path by combining directory path with file path
|
||||
const full_path = std.fmt.allocPrint(bun.default_allocator, "{s}/{s}", .{ this.directory_path, file_path }) catch {
|
||||
req.setYield(true);
|
||||
this.deref();
|
||||
return;
|
||||
};
|
||||
defer bun.default_allocator.free(full_path);
|
||||
|
||||
// Create a PathOrFileDescriptor from the full path
|
||||
const path_or_fd = JSC.Node.PathOrFileDescriptor{
|
||||
.path = JSC.Node.PathLike{
|
||||
.string = bun.PathString.init(full_path),
|
||||
},
|
||||
};
|
||||
|
||||
// Create a blob from the file path
|
||||
const store = Blob.Store.initFile(path_or_fd, null, bun.default_allocator) catch {
|
||||
req.setYield(true);
|
||||
this.deref();
|
||||
return;
|
||||
};
|
||||
|
||||
const blob = Blob.initWithStore(store, this.server.?.globalThis());
|
||||
|
||||
// Create a FileRoute to handle the actual file serving
|
||||
const file_route = FileRoute.initFromBlob(blob, .{
|
||||
.server = this.server,
|
||||
.status_code = 200,
|
||||
.headers = null,
|
||||
});
|
||||
|
||||
// Let the FileRoute handle the request
|
||||
file_route.on(req, resp, method);
|
||||
|
||||
// FileRoute will handle its own cleanup, so we just need to deref ourselves
|
||||
this.deref();
|
||||
}
|
||||
|
||||
const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{});
|
||||
pub const ref = RefCount.ref;
|
||||
pub const deref = RefCount.deref;
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("bun");
|
||||
const JSC = bun.JSC;
|
||||
const uws = bun.uws;
|
||||
const AnyServer = JSC.API.AnyServer;
|
||||
const Blob = JSC.WebCore.Blob;
|
||||
const FileRoute = @import("./FileRoute.zig");
|
||||
const AnyResponse = uws.AnyResponse;
|
||||
const strings = bun.strings;
|
||||
63
test/js/bun/http/directory-route-simple.test.ts
Normal file
63
test/js/bun/http/directory-route-simple.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles, isWindows } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("DirectoryRoute Simple", () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
testDir = tempDirWithFiles("directory-route-simple-test", {
|
||||
"index.html": "<html><body>Index Page</body></html>",
|
||||
"about.html": "<html><body>About Page</body></html>",
|
||||
});
|
||||
console.log("Test directory:", testDir);
|
||||
});
|
||||
|
||||
it("should recognize dir routes", () => {
|
||||
console.log("Testing directory route creation...");
|
||||
|
||||
try {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: testDir },
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Server created successfully on port:", server.port);
|
||||
console.log("Test directory exists:", Bun.file(join(testDir, "index.html")).size > 0);
|
||||
|
||||
server.stop();
|
||||
expect(true).toBe(true); // If we get here, route creation worked
|
||||
} catch (error) {
|
||||
console.error("Error creating server:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
it("should test basic file access", async () => {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: testDir },
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Server started on port:", server.port);
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${server.port}/`);
|
||||
console.log("Response status:", response.status);
|
||||
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (response.status !== 200) {
|
||||
const text = await response.text();
|
||||
console.log("Response body:", text);
|
||||
}
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
91
test/js/bun/http/directory-route-subpath.test.ts
Normal file
91
test/js/bun/http/directory-route-subpath.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles, isWindows } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("DirectoryRoute Subpath", () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
testDir = tempDirWithFiles("directory-route-subpath-test", {
|
||||
"index.html": "<html><body>Root Index</body></html>",
|
||||
"about.html": "<html><body>About Page</body></html>",
|
||||
"subdir/index.html": "<html><body>Subdir Index</body></html>",
|
||||
"subdir/page.html": "<html><body>Subdir Page</body></html>",
|
||||
});
|
||||
console.log("Test directory:", testDir);
|
||||
});
|
||||
|
||||
it("should handle root directory route", async () => {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: testDir },
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Server started on port:", server.port);
|
||||
|
||||
try {
|
||||
// Test root path
|
||||
const rootResponse = await fetch(`http://localhost:${server.port}/`);
|
||||
expect(rootResponse.status).toBe(200);
|
||||
const rootText = await rootResponse.text();
|
||||
expect(rootText).toContain("Root Index");
|
||||
|
||||
// Test sub-file
|
||||
const aboutResponse = await fetch(`http://localhost:${server.port}/about.html`);
|
||||
expect(aboutResponse.status).toBe(200);
|
||||
const aboutText = await aboutResponse.text();
|
||||
expect(aboutText).toContain("About Page");
|
||||
|
||||
// Test sub-directory
|
||||
const subdirResponse = await fetch(`http://localhost:${server.port}/subdir/`);
|
||||
expect(subdirResponse.status).toBe(200);
|
||||
const subdirText = await subdirResponse.text();
|
||||
expect(subdirText).toContain("Subdir Index");
|
||||
|
||||
// Test sub-directory file
|
||||
const subdirPageResponse = await fetch(`http://localhost:${server.port}/subdir/page.html`);
|
||||
expect(subdirPageResponse.status).toBe(200);
|
||||
const subdirPageText = await subdirPageResponse.text();
|
||||
expect(subdirPageText).toContain("Subdir Page");
|
||||
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle non-root directory route", async () => {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/files": { dir: testDir },
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Server started on port:", server.port);
|
||||
|
||||
try {
|
||||
// Test exact path match
|
||||
const exactResponse = await fetch(`http://localhost:${server.port}/files`);
|
||||
expect(exactResponse.status).toBe(200);
|
||||
const exactText = await exactResponse.text();
|
||||
expect(exactText).toContain("Root Index");
|
||||
|
||||
// Test sub-file
|
||||
const fileResponse = await fetch(`http://localhost:${server.port}/files/about.html`);
|
||||
expect(fileResponse.status).toBe(200);
|
||||
const fileText = await fileResponse.text();
|
||||
expect(fileText).toContain("About Page");
|
||||
|
||||
// Test sub-directory
|
||||
const subdirResponse = await fetch(`http://localhost:${server.port}/files/subdir/`);
|
||||
expect(subdirResponse.status).toBe(200);
|
||||
const subdirText = await subdirResponse.text();
|
||||
expect(subdirText).toContain("Subdir Index");
|
||||
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
46
test/js/bun/http/directory-route-wildcard-debug.test.ts
Normal file
46
test/js/bun/http/directory-route-wildcard-debug.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles, isWindows } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("DirectoryRoute Wildcard Debug", () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
testDir = tempDirWithFiles("directory-route-wildcard-debug", {
|
||||
"index.html": "<html><body>Root Index</body></html>",
|
||||
"test.html": "<html><body>Test Page</body></html>",
|
||||
});
|
||||
console.log("Test directory:", testDir);
|
||||
});
|
||||
|
||||
it("should debug wildcard behavior", async () => {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: testDir },
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Server started on port:", server.port);
|
||||
|
||||
try {
|
||||
// Test root path (this should work)
|
||||
console.log("Testing /");
|
||||
const rootResponse = await fetch(`http://localhost:${server.port}/`);
|
||||
console.log("/ status:", rootResponse.status);
|
||||
|
||||
// Test simple file (this should work with wildcard)
|
||||
console.log("Testing /test.html");
|
||||
const testResponse = await fetch(`http://localhost:${server.port}/test.html`);
|
||||
console.log("/test.html status:", testResponse.status);
|
||||
|
||||
if (testResponse.status !== 200) {
|
||||
const text = await testResponse.text();
|
||||
console.log("/test.html response body:", text);
|
||||
}
|
||||
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
225
test/js/bun/http/directory-route.test.ts
Normal file
225
test/js/bun/http/directory-route.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles, isWindows } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("DirectoryRoute", () => {
|
||||
let testDir: string;
|
||||
let server: Server;
|
||||
let port: number;
|
||||
|
||||
beforeAll(() => {
|
||||
testDir = tempDirWithFiles("directory-route-test", {
|
||||
"index.html": "<html><body>Index Page</body></html>",
|
||||
"index.htm": "<html><body>Index Htm Page</body></html>",
|
||||
"about.html": "<html><body>About Page</body></html>",
|
||||
"page.html": "<html><body>Page Content</body></html>",
|
||||
"script.js": "console.log('Hello from script');",
|
||||
"styles.css": "body { color: red; }",
|
||||
"robots.txt": "User-agent: *\nDisallow: /",
|
||||
"api/data.json": '{"message": "Hello from API"}',
|
||||
"assets/image.png": "fake-png-data",
|
||||
"nested/deep/file.txt": "Deep nested file content",
|
||||
"nested/index.html": "<html><body>Nested Index</body></html>",
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server?.stop();
|
||||
});
|
||||
|
||||
it("should serve index.html for root path", async () => {
|
||||
server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: testDir },
|
||||
},
|
||||
});
|
||||
port = server.port;
|
||||
|
||||
const response = await fetch(`http://localhost:${port}/`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/html");
|
||||
const text = await response.text();
|
||||
expect(text).toBe("<html><body>Index Page</body></html>");
|
||||
});
|
||||
|
||||
it("should serve index.html when accessing directory", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/nested/`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/html");
|
||||
const text = await response.text();
|
||||
expect(text).toBe("<html><body>Nested Index</body></html>");
|
||||
});
|
||||
|
||||
it("should serve direct file access", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/about.html`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/html");
|
||||
const text = await response.text();
|
||||
expect(text).toBe("<html><body>About Page</body></html>");
|
||||
});
|
||||
|
||||
it("should serve files with correct content-type", async () => {
|
||||
const jsResponse = await fetch(`http://localhost:${port}/script.js`);
|
||||
expect(jsResponse.status).toBe(200);
|
||||
expect(jsResponse.headers.get("content-type")).toContain("text/javascript");
|
||||
|
||||
const cssResponse = await fetch(`http://localhost:${port}/styles.css`);
|
||||
expect(cssResponse.status).toBe(200);
|
||||
expect(cssResponse.headers.get("content-type")).toContain("text/css");
|
||||
|
||||
const txtResponse = await fetch(`http://localhost:${port}/robots.txt`);
|
||||
expect(txtResponse.status).toBe(200);
|
||||
expect(txtResponse.headers.get("content-type")).toContain("text/plain");
|
||||
});
|
||||
|
||||
it("should try .html extension fallback", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/page`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/html");
|
||||
const text = await response.text();
|
||||
expect(text).toBe("<html><body>Page Content</body></html>");
|
||||
});
|
||||
|
||||
it("should serve nested files", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/api/data.json`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("application/json");
|
||||
const text = await response.text();
|
||||
expect(text).toBe('{"message": "Hello from API"}');
|
||||
});
|
||||
|
||||
it("should serve deeply nested files", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/nested/deep/file.txt`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/plain");
|
||||
const text = await response.text();
|
||||
expect(text).toBe("Deep nested file content");
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent files", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/nonexistent.html`);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should handle HEAD requests", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/about.html`, {
|
||||
method: "HEAD",
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/html");
|
||||
const text = await response.text();
|
||||
expect(text).toBe(""); // HEAD should not return body
|
||||
});
|
||||
|
||||
it("should have proper path traversal protection", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/../../../etc/passwd`);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should handle multiple directory routes", async () => {
|
||||
server?.stop();
|
||||
|
||||
const assetsDir = tempDirWithFiles("assets-test", {
|
||||
"logo.png": "fake-logo-data",
|
||||
"favicon.ico": "fake-favicon-data",
|
||||
});
|
||||
|
||||
server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: testDir },
|
||||
"/assets": { dir: assetsDir },
|
||||
},
|
||||
});
|
||||
port = server.port;
|
||||
|
||||
const mainResponse = await fetch(`http://localhost:${port}/about.html`);
|
||||
expect(mainResponse.status).toBe(200);
|
||||
const mainText = await mainResponse.text();
|
||||
expect(mainText).toBe("<html><body>About Page</body></html>");
|
||||
|
||||
const assetsResponse = await fetch(`http://localhost:${port}/assets/logo.png`);
|
||||
expect(assetsResponse.status).toBe(200);
|
||||
const assetsText = await assetsResponse.text();
|
||||
expect(assetsText).toBe("fake-logo-data");
|
||||
});
|
||||
|
||||
it("should support combined routes (directory + other route types)", async () => {
|
||||
server?.stop();
|
||||
|
||||
server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: testDir },
|
||||
"/api/hello": () => new Response("Hello from API"),
|
||||
},
|
||||
});
|
||||
port = server.port;
|
||||
|
||||
// Test directory route
|
||||
const fileResponse = await fetch(`http://localhost:${port}/about.html`);
|
||||
expect(fileResponse.status).toBe(200);
|
||||
const fileText = await fileResponse.text();
|
||||
expect(fileText).toBe("<html><body>About Page</body></html>");
|
||||
|
||||
// Test function route
|
||||
const apiResponse = await fetch(`http://localhost:${port}/api/hello`);
|
||||
expect(apiResponse.status).toBe(200);
|
||||
const apiText = await apiResponse.text();
|
||||
expect(apiText).toBe("Hello from API");
|
||||
});
|
||||
|
||||
it("should fall back to index.htm if index.html doesn't exist", async () => {
|
||||
const onlyHtmDir = tempDirWithFiles("only-htm-test", {
|
||||
"index.htm": "<html><body>Only HTM Index</body></html>",
|
||||
"page.html": "<html><body>Page Content</body></html>",
|
||||
});
|
||||
|
||||
server?.stop();
|
||||
|
||||
server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: onlyHtmDir },
|
||||
},
|
||||
});
|
||||
port = server.port;
|
||||
|
||||
const response = await fetch(`http://localhost:${port}/`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/html");
|
||||
const text = await response.text();
|
||||
expect(text).toBe("<html><body>Only HTM Index</body></html>");
|
||||
});
|
||||
|
||||
it("should handle directory access without trailing slash", async () => {
|
||||
const response = await fetch(`http://localhost:${port}/nested`, {
|
||||
redirect: "manual",
|
||||
});
|
||||
// Should try to find index.html in nested directory
|
||||
expect(response.status).toBe(200);
|
||||
const text = await response.text();
|
||||
expect(text).toBe("<html><body>Only HTM Index</body></html>"); // From the previous test setup
|
||||
});
|
||||
|
||||
it("should yield to next handler when file not found", async () => {
|
||||
server?.stop();
|
||||
|
||||
server = Bun.serve({
|
||||
port: 0,
|
||||
routes: {
|
||||
"/": { dir: testDir },
|
||||
},
|
||||
fetch: (req) => {
|
||||
return new Response("Fallback handler", { status: 404 });
|
||||
},
|
||||
});
|
||||
port = server.port;
|
||||
|
||||
const response = await fetch(`http://localhost:${port}/nonexistent.html`);
|
||||
expect(response.status).toBe(404);
|
||||
const text = await response.text();
|
||||
expect(text).toBe("Fallback handler");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user