const std = @import("std"); const Api = @import("../../../api/schema.zig").Api; const FilesystemRouter = @import("../../../router.zig"); const http = @import("../../../http.zig"); const JavaScript = @import("../javascript.zig"); const QueryStringMap = @import("../../../url.zig").QueryStringMap; const CombinedScanner = @import("../../../url.zig").CombinedScanner; const bun = @import("../../../global.zig"); const string = bun.string; const JSC = @import("../../../jsc.zig"); const js = JSC.C; const WebCore = @import("../webcore/response.zig"); const Router = @This(); const Bundler = @import("../../../bundler.zig"); const VirtualMachine = JavaScript.VirtualMachine; const ScriptSrcStream = std.io.FixedBufferStream([]u8); const ZigString = JSC.ZigString; const Fs = @import("../../../fs.zig"); const Base = @import("../base.zig"); const getAllocator = Base.getAllocator; const JSObject = JSC.JSObject; const JSError = Base.JSError; const JSValue = JSC.JSValue; const JSGlobalObject = JSC.JSGlobalObject; const strings = @import("strings"); const NewClass = Base.NewClass; const To = Base.To; const Request = WebCore.Request; const d = Base.d; const FetchEvent = WebCore.FetchEvent; const URLPath = @import("../../../http/url_path.zig"); const URL = @import("../../../url.zig").URL; route: *const FilesystemRouter.Match, route_holder: FilesystemRouter.Match = undefined, needs_deinit: bool = false, query_string_map: ?QueryStringMap = null, param_map: ?QueryStringMap = null, params_list_holder: FilesystemRouter.Param.List = .{}, pub fn importRoute( this: *Router, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, _: []const js.JSValueRef, _: js.ExceptionRef, ) js.JSObjectRef { const prom = JSC.JSModuleLoader.loadAndEvaluateModule(ctx.ptr(), &ZigString.init(this.route.file_path)); VirtualMachine.vm.tick(); return prom.result(ctx.ptr().vm()).asRef(); } pub fn match( _: void, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { if (arguments.len == 0) { JSError(getAllocator(ctx), "Expected string, FetchEvent, or Request but there were no arguments", .{}, ctx, exception); return null; } var arg: JSC.JSValue = undefined; if (FetchEvent.Class.loaded and js.JSValueIsObjectOfClass(ctx, arguments[0], FetchEvent.Class.get().*)) { var fetch_event = To.Zig.ptr(FetchEvent, arguments[0]); if (fetch_event.request_context != null) { return matchFetchEvent(ctx, fetch_event, exception); } // When disconencted, we still have a copy of the request data in here arg = JSC.JSValue.fromRef(fetch_event.getRequest(ctx, null, null, null)); } else { arg = JSC.JSValue.fromRef(arguments[0]); } var router = JavaScript.VirtualMachine.vm.bundler.router orelse { JSError(getAllocator(ctx), "Bun.match needs a framework configured with routes", .{}, ctx, exception); return null; }; var path_: ?ZigString.Slice = null; var pathname: string = ""; defer { if (path_) |path| { path.deinit(); } } if (arg.isString()) { var path_string = arg.getZigString(ctx.ptr()); path_ = path_string.toSlice(bun.default_allocator); var url = URL.parse(path_.?.slice()); pathname = url.pathname; } else if (arg.as(Request)) |req| { var path_string = req.url; path_ = path_string.toSlice(bun.default_allocator); var url = URL.parse(path_.?.slice()); pathname = url.pathname; } if (path_ == null) { JSError(getAllocator(ctx), "Expected string, FetchEvent, or Request", .{}, ctx, exception); return null; } const url_path = URLPath.parse(path_.?.slice()) catch { JSError(getAllocator(ctx), "Could not parse URL path", .{}, ctx, exception); return null; }; var match_params_fallback = std.heap.stackFallback(1024, bun.default_allocator); var match_params_allocator = match_params_fallback.get(); var match_params = FilesystemRouter.Param.List{}; match_params.ensureTotalCapacity(match_params_allocator, 16) catch unreachable; var prev_allocator = router.routes.allocator; router.routes.allocator = match_params_allocator; defer router.routes.allocator = prev_allocator; if (router.routes.matchPage("", url_path, &match_params)) |matched| { var match_ = matched; var params_list = match_.params.clone(bun.default_allocator) catch unreachable; var instance = getAllocator(ctx).create(Router) catch unreachable; instance.* = Router{ .route_holder = match_, .route = undefined, }; instance.params_list_holder = params_list; instance.route = &instance.route_holder; instance.route_holder.params = &instance.params_list_holder; return Instance.make(ctx, instance); } // router.routes.matchPage return JSC.JSValue.jsNull().asObjectRef(); } fn matchRequest( ctx: js.JSContextRef, request: *const Request, _: js.ExceptionRef, ) js.JSObjectRef { return createRouteObject(ctx, request.request_context); } fn matchFetchEvent( ctx: js.JSContextRef, fetch_event: *const FetchEvent, _: js.ExceptionRef, ) js.JSObjectRef { return createRouteObject(ctx, fetch_event.request_context.?); } fn createRouteObject(ctx: js.JSContextRef, req: *const http.RequestContext) js.JSValueRef { const route = &(req.matched_route orelse { return js.JSValueMakeNull(ctx); }); return createRouteObjectFromMatch(ctx, route); } fn createRouteObjectFromMatch( ctx: js.JSContextRef, route: *const FilesystemRouter.Match, ) js.JSValueRef { var router = getAllocator(ctx).create(Router) catch unreachable; router.* = Router{ .route = route, }; return Instance.make(ctx, router); } pub const match_type_definition = &[_]d.ts{ .{ .tsdoc = "Match a {@link https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent FetchEvent} to a `Route` from the local filesystem. Returns `null` if there is no match.", .args = &[_]d.ts.arg{ .{ .name = "event", .@"return" = "FetchEvent", }, }, .@"return" = "Route | null", }, .{ .tsdoc = "Match a `pathname` to a `Route` from the local filesystem. Returns `null` if there is no match.", .args = &[_]d.ts.arg{ .{ .name = "pathname", .@"return" = "string", }, }, .@"return" = "Route | null", }, .{ .tsdoc = "Match a {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Request} to a `Route` from the local filesystem. Returns `null` if there is no match.", .args = &[_]d.ts.arg{ .{ .name = "request", .@"return" = "Request", }, }, .@"return" = "Route | null", }, }; pub const Instance = NewClass( Router, .{ .name = "Route", .read_only = true, .ts = .{ .class = d.ts.class{ .tsdoc = \\Route matched from the filesystem. , }, }, }, .{ .finalize = finalize, .import = .{ .rfn = importRoute, .ts = d.ts{ .@"return" = "Object", .tsdoc = \\Synchronously load & evaluate the file corresponding to the route. Returns the exports of the route. This is similar to `await import(route.filepath)`, except it's synchronous. It is recommended to use this function instead of `import`. , }, }, }, .{ .pathname = .{ .get = getPathname, .ro = true, .ts = d.ts{ .@"return" = "string", .@"tsdoc" = "URL path as appears in a web browser's address bar", }, }, .filePath = .{ .get = getFilePath, .ro = true, .ts = d.ts{ .@"return" = "string", .tsdoc = \\Project-relative filesystem path to the route file. , }, }, .scriptSrc = .{ .get = getScriptSrc, .ro = true, .ts = d.ts{ .@"return" = "string", .tsdoc = \\src attribute of the script tag that loads the route. , }, }, .kind = .{ .get = getKind, .ro = true, .ts = d.ts{ .@"return" = "\"exact\" | \"dynamic\" | \"catch-all\" | \"optional-catch-all\"", }, }, .name = .{ .get = getRoute, .ro = true, .ts = d.ts{ .@"return" = "string", .tsdoc = \\Route name \\@example \\`"blog/posts/[id]"` \\`"blog/posts/[id]/[[...slug]]"` \\`"blog"` , }, }, .query = .{ .get = getQuery, .ro = true, .ts = d.ts{ .@"return" = "Record", .tsdoc = \\Route parameters & parsed query string values as a key-value object \\ \\@example \\```js \\console.assert(router.query.id === "123"); \\console.assert(router.pathname === "/blog/posts/123"); \\console.assert(router.route === "blog/posts/[id]"); \\``` , }, }, .params = .{ .get = getParams, .ro = true, .ts = d.ts{ .@"return" = "Record", .tsdoc = \\Route parameters as a key-value object \\ \\@example \\```js \\console.assert(router.query.id === "123"); \\console.assert(router.pathname === "/blog/posts/123"); \\console.assert(router.route === "blog/posts/[id]"); \\``` , }, }, }, ); pub fn getFilePath( this: *Router, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(this.route.file_path) .withEncoding() .toValueGC(ctx.ptr()).asRef(); } pub fn finalize( this: *Router, ) void { if (this.query_string_map) |*map| { map.deinit(); } if (this.needs_deinit) { this.params_list_holder.deinit(bun.default_allocator); this.params_list_holder = .{}; this.needs_deinit = false; } bun.default_allocator.destroy(this); } pub fn getPathname( this: *Router, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(this.route.pathname) .withEncoding() .toValueGC(ctx.ptr()).asRef(); } pub fn getRoute( this: *Router, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(this.route.name) .withEncoding() .toValueGC(ctx.ptr()).asRef(); } const KindEnum = struct { pub const exact = "exact"; pub const catch_all = "catch-all"; pub const optional_catch_all = "optional-catch-all"; pub const dynamic = "dynamic"; // this is kinda stupid it should maybe just store it pub fn init(name: string) ZigString { if (strings.contains(name, "[[...")) { return ZigString.init(optional_catch_all); } else if (strings.contains(name, "[...")) { return ZigString.init(catch_all); } else if (strings.contains(name, "[")) { return ZigString.init(dynamic); } else { return ZigString.init(exact); } } }; pub fn getKind( this: *Router, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { return KindEnum.init(this.route.name).toValue(ctx.ptr()).asRef(); } threadlocal var query_string_values_buf: [256]string = undefined; threadlocal var query_string_value_refs_buf: [256]ZigString = undefined; pub fn createQueryObject(ctx: js.JSContextRef, map: *QueryStringMap, _: js.ExceptionRef) callconv(.C) js.JSValueRef { const QueryObjectCreator = struct { query: *QueryStringMap, pub fn create(this: *@This(), obj: *JSObject, global: *JSGlobalObject) void { var iter = this.query.iter(); var str: ZigString = undefined; while (iter.next(&query_string_values_buf)) |entry| { str = ZigString.init(entry.name); std.debug.assert(entry.values.len > 0); if (entry.values.len > 1) { var values = query_string_value_refs_buf[0..entry.values.len]; for (entry.values) |value, i| { values[i] = ZigString.init(value); } obj.putRecord(global, &str, values.ptr, values.len); } else { query_string_value_refs_buf[0] = ZigString.init(entry.values[0]); obj.putRecord(global, &str, &query_string_value_refs_buf, 1); } } } }; var creator = QueryObjectCreator{ .query = map }; var value = JSObject.createWithInitializer(QueryObjectCreator, &creator, ctx.ptr(), map.getNameCount()); return value.asRef(); } pub fn getScriptSrcString( comptime Writer: type, writer: Writer, file_path: string, client_framework_enabled: bool, ) void { var entry_point_tempbuf: [bun.MAX_PATH_BYTES]u8 = undefined; // We don't store the framework config including the client parts in the server // instead, we just store a boolean saying whether we should generate this whenever the script is requested // this is kind of bad. we should consider instead a way to inline the contents of the script. if (client_framework_enabled) { JavaScript.Bun.getPublicPath( Bundler.ClientEntryPoint.generateEntryPointPath( &entry_point_tempbuf, Fs.PathName.init(file_path), ), VirtualMachine.vm.origin, Writer, writer, ); } else { JavaScript.Bun.getPublicPath(file_path, VirtualMachine.vm.origin, Writer, writer); } } pub fn getScriptSrc( this: *Router, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, _: js.ExceptionRef, ) js.JSValueRef { var script_src_buffer = std.ArrayList(u8).init(bun.default_allocator); var writer = script_src_buffer.writer(); getScriptSrcString(@TypeOf(&writer), &writer, this.route.file_path, this.route.client_framework_enabled); return ZigString.init(script_src_buffer.toOwnedSlice()).toExternalValue(ctx.ptr()).asObjectRef(); } pub fn getParams( this: *Router, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { if (this.param_map == null) { if (this.route.params.len > 0) { if (QueryStringMap.initWithScanner(getAllocator(ctx), CombinedScanner.init( "", this.route.pathnameWithoutLeadingSlash(), this.route.name, this.route.params, ))) |map| { this.param_map = map; } else |_| {} } } // If it's still null, there are no params if (this.param_map) |*map| { return createQueryObject(ctx, map, exception); } else { return JSValue.createEmptyObject(ctx.ptr(), 0).asRef(); } } pub fn getQuery( this: *Router, ctx: js.JSContextRef, _: js.JSObjectRef, _: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { if (this.query_string_map == null) { if (this.route.params.len > 0) { if (QueryStringMap.initWithScanner(getAllocator(ctx), CombinedScanner.init( this.route.query_string, this.route.pathnameWithoutLeadingSlash(), this.route.name, this.route.params, ))) |map| { this.query_string_map = map; } else |_| {} } else if (this.route.query_string.len > 0) { if (QueryStringMap.init(getAllocator(ctx), this.route.query_string)) |map| { this.query_string_map = map; } else |_| {} } } // If it's still null, the query string has no names. if (this.query_string_map) |*map| { return createQueryObject(ctx, map, exception); } else { return JSValue.createEmptyObject(ctx.ptr(), 0).asRef(); } }