usingnamespace @import("../base.zig"); 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("../../../query_string_map.zig").QueryStringMap; const CombinedScanner = @import("../../../query_string_map.zig").CombinedScanner; usingnamespace @import("../bindings/bindings.zig"); usingnamespace @import("../webcore/response.zig"); const Router = @This(); const Bundler = @import("../../../bundler.zig"); const VirtualMachine = JavaScript.VirtualMachine; const ScriptSrcStream = std.io.FixedBufferStream([]u8); const Fs = @import("../../../fs.zig"); route: *const FilesystemRouter.Match, query_string_map: ?QueryStringMap = null, param_map: ?QueryStringMap = null, script_src: ?string = null, script_src_buf: [1024]u8 = undefined, script_src_buf_writer: ScriptSrcStream = undefined, pub fn importRoute( this: *Router, ctx: js.JSContextRef, function: js.JSObjectRef, thisObject: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { const prom = JSModuleLoader.loadAndEvaluateModule(VirtualMachine.vm.global, ZigString.init(this.route.file_path)); VirtualMachine.vm.global.vm().drainMicrotasks(); return prom.result(VirtualMachine.vm.global.vm()).asRef(); } pub fn match( obj: void, ctx: js.JSContextRef, function: js.JSObjectRef, thisObject: 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; } if (js.JSValueIsObjectOfClass(ctx, arguments[0], FetchEvent.Class.get().*)) { return matchFetchEvent(ctx, To.Zig.ptr(FetchEvent, arguments[0]), exception); } if (js.JSValueIsString(ctx, arguments[0])) { return matchPathName(ctx, arguments[0], exception); } if (js.JSValueIsObjectOfClass(ctx, arguments[0], Request.Class.get().*)) { return matchRequest(ctx, To.Zig.ptr(Request, arguments[0]), exception); } return null; } fn matchRequest( ctx: js.JSContextRef, request: *const Request, exception: js.ExceptionRef, ) js.JSObjectRef { return createRouteObject(ctx, request.request_context, exception); } fn matchPathNameString( ctx: js.JSContextRef, pathname: string, exception: js.ExceptionRef, ) js.JSObjectRef {} fn matchPathName( ctx: js.JSContextRef, pathname: js.JSStringRef, exception: js.ExceptionRef, ) js.JSObjectRef { return null; } fn matchFetchEvent( ctx: js.JSContextRef, fetch_event: *const FetchEvent, exception: js.ExceptionRef, ) js.JSObjectRef { return createRouteObject(ctx, fetch_event.request_context, exception); } fn createRouteObject(ctx: js.JSContextRef, req: *const http.RequestContext, exception: js.ExceptionRef) js.JSValueRef { const route = &(req.matched_route orelse { return js.JSValueMakeNull(ctx); }); var router = getAllocator(ctx).create(Router) catch unreachable; router.* = Router{ .route = route, }; router.script_src_buf_writer = ScriptSrcStream{ .pos = 0, .buffer = std.mem.span(&router.script_src_buf) }; 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 = .{ .rfn = 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, thisObject: js.JSObjectRef, prop: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(this.route.file_path).toValue(VirtualMachine.vm.global).asRef(); } pub fn finalize( this: *Router, ) void { if (this.query_string_map) |*map| { map.deinit(); } } pub fn getPathname( this: *Router, ctx: js.JSContextRef, thisObject: js.JSObjectRef, prop: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(this.route.pathname).toValue(VirtualMachine.vm.global).asRef(); } pub fn getRoute( this: *Router, ctx: js.JSContextRef, thisObject: js.JSObjectRef, prop: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { return ZigString.init(this.route.name).toValue(VirtualMachine.vm.global).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, thisObject: js.JSObjectRef, prop: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { return KindEnum.init(this.route.name).toValue(VirtualMachine.vm.global).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, exception: 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(VirtualMachine.vm.global, &str, values.ptr, values.len); } else { query_string_value_refs_buf[0] = ZigString.init(entry.values[0]); obj.putRecord(VirtualMachine.vm.global, &str, &query_string_value_refs_buf, 1); } } } }; var creator = QueryObjectCreator{ .query = map }; var value = JSObject.createWithInitializer(QueryObjectCreator, &creator, VirtualMachine.vm.global, map.getNameCount()); return value.asRef(); } threadlocal var entry_point_tempbuf: [std.fs.MAX_PATH_BYTES]u8 = undefined; pub fn getScriptSrcString( comptime Writer: type, writer: Writer, file_path: string, client_framework_enabled: bool, ) void { // 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), ), ScriptSrcStream.Writer, writer, ); } else { JavaScript.Bun.getPublicPath(file_path, ScriptSrcStream.Writer, writer); } } pub fn getScriptSrc( this: *Router, ctx: js.JSContextRef, thisObject: js.JSObjectRef, prop: js.JSStringRef, exception: js.ExceptionRef, ) js.JSValueRef { const src = this.script_src orelse brk: { getScriptSrcString(ScriptSrcStream.Writer, this.script_src_buf_writer.writer(), this.route.file_path, this.route.client_framework_enabled); break :brk this.script_src_buf[0..this.script_src_buf_writer.pos]; }; this.script_src = src; return js.JSValueMakeString(ctx, ZigString.init(src).toJSStringRef()); } pub fn getParams( this: *Router, ctx: js.JSContextRef, thisObject: js.JSObjectRef, prop: 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 |err| {} } } // If it's still null, there are no params if (this.param_map) |*map| { return createQueryObject(ctx, map, exception); } else { return JSValue.createEmptyObject(VirtualMachine.vm.global, 0).asRef(); } } pub fn getQuery( this: *Router, ctx: js.JSContextRef, thisObject: js.JSObjectRef, prop: 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 |err| {} } else if (this.route.query_string.len > 0) { if (QueryStringMap.init(getAllocator(ctx), this.route.query_string)) |map| { this.query_string_map = map; } else |err| {} } } // 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(VirtualMachine.vm.global, 0).asRef(); } }