diff options
-rw-r--r-- | src/bun.js/api/filesystem_router.zig | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 4 | ||||
-rw-r--r-- | src/global.zig | 11 | ||||
-rw-r--r-- | src/string_immutable.zig | 29 | ||||
-rw-r--r-- | src/url.zig | 13 | ||||
-rw-r--r-- | test/bun.js/filesystem_router.test.ts | 95 |
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`); + } +}); |