aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bun.js/api/filesystem_router.zig5
-rw-r--r--src/bun.js/bindings/bindings.zig4
-rw-r--r--src/global.zig11
-rw-r--r--src/string_immutable.zig29
-rw-r--r--src/url.zig13
-rw-r--r--test/bun.js/filesystem_router.test.ts95
6 files changed, 142 insertions, 15 deletions
diff --git a/src/bun.js/api/filesystem_router.zig b/src/bun.js/api/filesystem_router.zig
index 96c094b5c..f43a30d4e 100644
--- a/src/bun.js/api/filesystem_router.zig
+++ b/src/bun.js/api/filesystem_router.zig
@@ -290,6 +290,7 @@ pub const FileSystemRouter = struct {
.arena = arena,
.allocator = allocator,
};
+ router.config.dir = fs_router.base_dir.?.slice();
fs_router.base_dir.?.ref();
return fs_router;
}
@@ -701,8 +702,10 @@ pub const MatchedRoute = struct {
this: *MatchedRoute,
globalThis: *JSC.JSGlobalObject,
) callconv(.C) JSC.JSValue {
- if (this.route.query_string.len == 0) {
+ if (this.route.query_string.len == 0 and this.route.params.len == 0) {
return JSValue.createEmptyObject(globalThis, 0);
+ } else if (this.route.query_string.len == 0) {
+ return this.getParams(globalThis);
}
if (this.query_string_map == null) {
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index c88e0ba8f..b769625ec 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -236,8 +236,8 @@ pub const ZigString = extern struct {
}
pub fn cloneWithTrailingSlash(this: Slice, allocator: std.mem.Allocator) !Slice {
- var duped = try std.fmt.allocPrintZ(allocator, "{s}/", .{strings.withoutTrailingSlash(this.slice())});
- return Slice{ .allocator = NullableAllocator.init(allocator), .ptr = duped.ptr, .len = @truncate(u32, duped.len) };
+ var buf = try strings.cloneNormalizingSeparators(allocator, this.slice());
+ return Slice{ .allocator = NullableAllocator.init(allocator), .ptr = buf.ptr, .len = @truncate(u32, buf.len) };
}
pub fn cloneZ(this: Slice, allocator: std.mem.Allocator) !Slice {
diff --git a/src/global.zig b/src/global.zig
index c75a72891..32ef4214f 100644
--- a/src/global.zig
+++ b/src/global.zig
@@ -413,3 +413,14 @@ pub const Mimalloc = @import("./allocators/mimalloc.zig");
pub fn isSliceInBuffer(slice: []const u8, buffer: []const u8) bool {
return slice.len > 0 and @ptrToInt(buffer.ptr) <= @ptrToInt(slice.ptr) and ((@ptrToInt(slice.ptr) + slice.len) <= (@ptrToInt(buffer.ptr) + buffer.len));
}
+
+pub fn rangeOfSliceInBuffer(slice: []const u8, buffer: []const u8) ?[2]u32 {
+ if (!isSliceInBuffer(slice, buffer)) return null;
+ const r = [_]u32{
+ @truncate(u32, @ptrToInt(slice.ptr) -| @ptrToInt(buffer.ptr)),
+ @truncate(u32, slice.len),
+ };
+ if (comptime Environment.allow_assert)
+ std.debug.assert(strings.eqlLong(slice, buffer[r[0]..][0..r[1]], false));
+ return r;
+}
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index 654080f8a..5acc5befd 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -3762,3 +3762,32 @@ pub fn isIPAddress(input: []const u8) bool {
return false;
}
}
+
+pub fn cloneNormalizingSeparators(
+ allocator: std.mem.Allocator,
+ input: []const u8,
+) ![]u8 {
+ // remove duplicate slashes in the file path
+ var base = withoutTrailingSlash(input);
+ var tokenized = std.mem.tokenize(u8, base, std.fs.path.sep_str);
+ var buf = try allocator.alloc(u8, base.len + 2);
+ std.debug.assert(base.len > 0);
+ if (base[0] == std.fs.path.sep) {
+ buf[0] = std.fs.path.sep;
+ }
+ var remain = buf[@as(usize, @boolToInt(base[0] == std.fs.path.sep))..];
+
+ while (tokenized.next()) |token| {
+ if (token.len == 0) continue;
+ std.mem.copy(u8, remain, token);
+ remain[token.len..][0] = std.fs.path.sep;
+ remain = remain[token.len + 1 ..];
+ }
+ if ((remain.ptr - 1) != buf.ptr and (remain.ptr - 1)[0] != std.fs.path.sep) {
+ remain[0] = std.fs.path.sep;
+ remain = remain[1..];
+ }
+ remain[0] = 0;
+
+ return buf[0 .. @ptrToInt(remain.ptr) - @ptrToInt(buf.ptr)];
+}
diff --git a/src/url.zig b/src/url.zig
index 97ef74125..26c606a4d 100644
--- a/src/url.zig
+++ b/src/url.zig
@@ -859,15 +859,14 @@ pub const CombinedScanner = struct {
fn stringPointerFromStrings(parent: string, in: string) Api.StringPointer {
if (in.len == 0 or parent.len == 0) return Api.StringPointer{};
- if (bun.isSliceInBuffer(in, parent)) {
- const offset = @minimum(
- @maximum(@ptrToInt(in.ptr), @ptrToInt(parent.ptr)) - @minimum(@ptrToInt(in.ptr), @ptrToInt(parent.ptr)),
- @minimum(in.len, parent.len),
- );
-
- return Api.StringPointer{ .offset = @truncate(u32, offset), .length = @truncate(u32, in.len) };
+ if (bun.rangeOfSliceInBuffer(in, parent)) |range| {
+ return Api.StringPointer{ .offset = range[0], .length = range[1] };
} else {
if (strings.indexOf(parent, in)) |i| {
+ if (comptime Environment.allow_assert) {
+ std.debug.assert(strings.eqlLong(parent[i..][0..in.len], in, false));
+ }
+
return Api.StringPointer{
.offset = @truncate(u32, i),
.length = @truncate(u32, in.len),
diff --git a/test/bun.js/filesystem_router.test.ts b/test/bun.js/filesystem_router.test.ts
index 41ce0dc11..168543e41 100644
--- a/test/bun.js/filesystem_router.test.ts
+++ b/test/bun.js/filesystem_router.test.ts
@@ -101,7 +101,22 @@ it("should handle empty dirs", () => {
expect(Object.values(routes).length).toBe(0);
});
-it("should support dynamic routes", () => {
+it("should match dynamic routes", () => {
+ // set up the test
+ const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx"]);
+
+ const router = new Bun.FileSystemRouter({
+ dir,
+ style: "nextjs",
+ });
+
+ const { name, filePath } = router.match("/posts/hello-world");
+
+ expect(name).toBe("/posts/[id]");
+ expect(filePath).toBe(`${dir}/posts/[id].tsx`);
+});
+
+it(".params works on dynamic routes", () => {
// set up the test
const { dir } = make(["index.tsx", "posts/[id].tsx", "posts.tsx"]);
@@ -111,14 +126,10 @@ it("should support dynamic routes", () => {
});
const {
- name,
params: { id },
- filePath,
} = router.match("/posts/hello-world");
expect(id).toBe("hello-world");
- expect(name).toBe("/posts/[id]");
- expect(filePath).toBe(`${dir}/[id].tsx`);
});
it("should support static routes", () => {
@@ -302,3 +313,77 @@ it("assetPrefix, src, and origin", async () => {
expect(filePath).toBe(`${dir}/posts/[id].tsx`);
}
});
+
+it(".query works", () => {
+ // set up the test
+ const { dir } = make(["posts.tsx"]);
+
+ const router = new Bun.FileSystemRouter({
+ dir,
+ style: "nextjs",
+ assetPrefix: "/_next/static/",
+ origin: "https://nextjs.org",
+ });
+
+ for (let [current, object] of [
+ [new URL("https://example.com/posts?hello=world").href, { hello: "world" }],
+ [
+ new URL("https://example.com/posts?hello=world&second=2").href,
+ { hello: "world", second: "2" },
+ ],
+ [
+ new URL("https://example.com/posts?hello=world&second=2&third=3").href,
+ { hello: "world", second: "2", third: "3" },
+ ],
+ [new URL("https://example.com/posts").href, {}],
+ ]) {
+ const { name, src, filePath, checkThisDoesntCrash, query } =
+ router.match(current);
+ expect(name).toBe("/posts");
+
+ // check nothing is weird on the MatchedRoute object
+ expect(checkThisDoesntCrash).toBeUndefined();
+
+ expect(JSON.stringify(query)).toBe(JSON.stringify(object));
+ expect(filePath).toBe(`${dir}/posts.tsx`);
+ }
+});
+
+it(".query works with dynamic routes, including params", () => {
+ // set up the test
+ const { dir } = make(["posts/[id].tsx"]);
+
+ const router = new Bun.FileSystemRouter({
+ dir,
+ style: "nextjs",
+ assetPrefix: "/_next/static/",
+ origin: "https://nextjs.org",
+ });
+
+ for (let [current, object] of [
+ [
+ new URL("https://example.com/posts/123?hello=world").href,
+ { id: "123", hello: "world" },
+ ],
+ [
+ new URL("https://example.com/posts/123?hello=world&second=2").href,
+ { id: "123", hello: "world", second: "2" },
+ ],
+ [
+ new URL("https://example.com/posts/123?hello=world&second=2&third=3")
+ .href,
+ { id: "123", hello: "world", second: "2", third: "3" },
+ ],
+ [new URL("https://example.com/posts/123").href, { id: "123" }],
+ ]) {
+ const { name, src, filePath, checkThisDoesntCrash, query } =
+ router.match(current);
+ expect(name).toBe("/posts/[id]");
+
+ // check nothing is weird on the MatchedRoute object
+ expect(checkThisDoesntCrash).toBeUndefined();
+
+ expect(JSON.stringify(query)).toBe(JSON.stringify(object));
+ expect(filePath).toBe(`${dir}/posts/[id].tsx`);
+ }
+});