aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-09-14 01:28:04 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-09-14 01:28:04 -0700
commitbdd5502aefcdc388ebbfa563131417314acd32d1 (patch)
treead2ed525534e70a3b31f0ee9904fcc716dc8d7b8
parent2f301e39d78ab5f70b6c5468f8ab21e3a201e315 (diff)
downloadbun-bdd5502aefcdc388ebbfa563131417314acd32d1.tar.gz
bun-bdd5502aefcdc388ebbfa563131417314acd32d1.tar.zst
bun-bdd5502aefcdc388ebbfa563131417314acd32d1.zip
SPAs now work by default when there's a public/index.html file
-rw-r--r--src/http.zig114
-rw-r--r--src/options.zig25
2 files changed, 118 insertions, 21 deletions
diff --git a/src/http.zig b/src/http.zig
index 2abdf849e..a38a50c46 100644
--- a/src/http.zig
+++ b/src/http.zig
@@ -317,14 +317,20 @@ pub const RequestContext = struct {
// std.mem.copy(u8, &tmp_buildfile_buf, relative_unrooted_path);
// std.mem.copy(u8, tmp_buildfile_buf[relative_unrooted_path.len..], "/"
// Search for /index.html
- if (public_dir.openFile("index.html", .{})) |file| {
+ if (this.bundler.options.routes.single_page_app_routing and
+ this.bundler.options.routes.single_page_app_fd != 0)
+ {
+ this.sendSinglePageHTML() catch {};
+ return null;
+ } else if (public_dir.openFile("index.html", .{})) |file| {
var index_path = "index.html".*;
relative_unrooted_path = &(index_path);
_file = file;
extension = "html";
} else |err| {}
+
// Okay is it actually a full path?
- } else {
+ } else if (extension.len > 0) {
if (public_dir.openFile(relative_unrooted_path, .{})) |file| {
_file = file;
} else |err| {}
@@ -630,6 +636,35 @@ pub const RequestContext = struct {
);
}
+ pub fn sendSinglePageHTML(ctx: *RequestContext) !void {
+ ctx.appendHeader("Content-Type", MimeType.html.value);
+ ctx.appendHeader("Cache-Control", "no-cache");
+
+ defer ctx.done();
+
+ std.debug.assert(ctx.bundler.options.routes.single_page_app_fd > 0);
+ const file = std.fs.File{ .handle = ctx.bundler.options.routes.single_page_app_fd };
+ const stats = file.stat() catch |err| {
+ Output.prettyErrorln("<r><red>Error {s}<r> reading index.html", .{@errorName(err)});
+ ctx.writeStatus(500) catch {};
+ return;
+ };
+
+ const content_length = stats.size;
+ try ctx.writeStatus(200);
+ try ctx.prepareToSendBody(content_length, false);
+
+ _ = try std.os.sendfile(
+ ctx.conn.client.socket.fd,
+ ctx.bundler.options.routes.single_page_app_fd,
+ 0,
+ content_length,
+ &[_]std.os.iovec_const{},
+ &[_]std.os.iovec_const{},
+ 0,
+ );
+ }
+
pub const WatchBuilder = struct {
watcher: *Watcher,
bundler: *Bundler,
@@ -2501,13 +2536,30 @@ pub const Server = struct {
// However, we want to optimize for easy to copy paste
// Nobody should get weird CORS errors when you go to the printed url.
if (std.mem.readIntNative(u32, &addr.ipv4.host.octets) == 0 or std.mem.readIntNative(u128, &addr.ipv6.host.octets) == 0) {
- Output.prettyError(" Bun!!<r>\n\n\n<d> Link:<r> <b><cyan>http://localhost:{d}<r>\n\n\n", .{
- addr.ipv4.port,
- });
+ if (server.bundler.options.routes.single_page_app_routing) {
+ Output.prettyError(
+ " Bun!!<r>\n\n\n<d> Link:<r> <b><cyan>http://localhost:{d}<r>\n <d>./{s}/index.html<r> \n\n\n",
+ .{
+ addr.ipv4.port,
+ resolve_path.relative(server.bundler.fs.top_level_dir, server.bundler.options.routes.static_dir),
+ },
+ );
+ } else {
+ Output.prettyError(" Bun!!<r>\n\n\n<d> Link:<r> <b><cyan>http://localhost:{d}<r>\n\n\n", .{
+ addr.ipv4.port,
+ });
+ }
} else {
- Output.prettyError(" Bun!!<r>\n\n\n<d> Link:<r> <b><cyan>http://{s}<r>\n\n\n", .{
- addr,
- });
+ if (server.bundler.options.routes.single_page_app_routing) {
+ Output.prettyError(" Bun!!<r>\n\n\n<d> Link:<r> <b><cyan>http://{s}<r>\n <d>./{s}/index.html<r> \n\n\n", .{
+ addr,
+ resolve_path.relative(server.bundler.fs.top_level_dir, server.bundler.options.routes.static_dir),
+ });
+ } else {
+ Output.prettyError(" Bun!!<r>\n\n\n<d> Link:<r> <b><cyan>http://{s}<r>\n\n\n", .{
+ addr,
+ });
+ }
}
Output.flush();
@@ -2698,6 +2750,8 @@ pub const Server = struct {
return;
};
}
+
+ finished = finished or req_ctx.has_called_done;
}
},
else => {},
@@ -2720,19 +2774,21 @@ pub const Server = struct {
finished = true;
}
} else {
- if (!finished) {
- req_ctx.handleRequest() catch |err| {
- switch (err) {
- error.ModuleNotFound => {
- req_ctx.sendNotFound() catch {};
- },
- else => {
- Output.printErrorln("FAIL [{s}] - {s}: {s}", .{ @errorName(err), req.method, req.path });
- did_print = true;
- },
- }
- };
- finished = true;
+ request_handler: {
+ if (!finished) {
+ req_ctx.handleRequest() catch |err| {
+ switch (err) {
+ error.ModuleNotFound => {
+ break :request_handler;
+ },
+ else => {
+ Output.printErrorln("FAIL [{s}] - {s}: {s}", .{ @errorName(err), req.method, req.path });
+ did_print = true;
+ },
+ }
+ };
+ finished = true;
+ }
}
}
@@ -2745,8 +2801,24 @@ pub const Server = struct {
did_print = true;
};
}
+
+ finished = finished or req_ctx.has_called_done;
}
}
+
+ if (comptime features.public_folder != .none) {
+ if (!finished and (req_ctx.bundler.options.routes.single_page_app_routing and req_ctx.url.extname.len == 0)) {
+ req_ctx.sendSinglePageHTML() catch |err| {
+ Output.printErrorln("FAIL [{s}] - {s}: {s}", .{ @errorName(err), req.method, req.path });
+ did_print = true;
+ };
+ finished = true;
+ }
+ }
+
+ if (!finished) {
+ req_ctx.sendNotFound() catch {};
+ }
}
pub fn initWatcher(server: *Server) !void {
diff --git a/src/options.zig b/src/options.zig
index 240ceeaaf..5035f4c93 100644
--- a/src/options.zig
+++ b/src/options.zig
@@ -1153,6 +1153,29 @@ pub const BundleOptions = struct {
};
opts.routes.static_dir_enabled = opts.routes.static_dir_handle != null;
}
+
+ if (opts.routes.static_dir_enabled and (opts.framework == null or !opts.framework.?.server.isEnabled()) and !opts.routes.routes_enabled) {
+ const dir = opts.routes.static_dir_handle.?;
+ var index_html_file = dir.openFile("index.html", .{ .read = true }) catch |err| brk: {
+ switch (err) {
+ error.FileNotFound => {},
+ else => {
+ Output.prettyErrorln(
+ "{s} when trying to open {s}/index.html. single page app routing is disabled.",
+ .{ @errorName(err), opts.routes.static_dir },
+ );
+ },
+ }
+ opts.routes.single_page_app_routing = false;
+ break :brk null;
+ };
+
+ if (index_html_file) |index_dot_html| {
+ opts.routes.single_page_app_routing = true;
+ opts.routes.single_page_app_fd = index_dot_html.handle;
+ }
+ }
+
// Windows has weird locking rules for file access.
// so it's a bad idea to keep a file handle open for a long time on Windows.
if (isWindows and opts.routes.static_dir_handle != null) {
@@ -1763,6 +1786,8 @@ pub const RouteConfig = struct {
static_dir: string = "",
static_dir_handle: ?std.fs.Dir = null,
static_dir_enabled: bool = false,
+ single_page_app_routing: bool = false,
+ single_page_app_fd: StoredFileDescriptorType = 0,
pub fn toAPI(this: *const RouteConfig) Api.LoadedRouteConfig {
return .{