diff options
Diffstat (limited to '')
-rw-r--r-- | src/env_loader.zig | 2 | ||||
-rw-r--r-- | src/js_lexer.zig | 2 | ||||
-rw-r--r-- | src/logger.zig | 9 | ||||
-rw-r--r-- | src/resolver/resolve_path.zig | 6 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 4 | ||||
-rw-r--r-- | src/router.zig | 1223 | ||||
-rw-r--r-- | src/string_immutable.zig | 57 | ||||
-rw-r--r-- | src/string_types.zig | 43 |
8 files changed, 1203 insertions, 143 deletions
diff --git a/src/env_loader.zig b/src/env_loader.zig index 7b1acdc95..7fc2cbd5c 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -325,7 +325,7 @@ pub const Lexer = struct { pub fn init(source: *const logger.Source) Lexer { return Lexer{ .source = source, - .iter = CodepointIterator{ .bytes = source.contents, .i = 0 }, + .iter = CodepointIterator.init(source.contents), }; } }; diff --git a/src/js_lexer.zig b/src/js_lexer.zig index f5417d0f4..fe4bf6a12 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -2692,7 +2692,7 @@ pub fn isIdentifier(text: string) bool { return false; } - var iter = strings.CodepointIterator{ .i = 0, .bytes = text }; + var iter = strings.CodepointIterator.init(text); if (!isIdentifierStart(iter.nextCodepoint())) { return false; diff --git a/src/logger.zig b/src/logger.zig index 5926334aa..60e22fbf7 100644 --- a/src/logger.zig +++ b/src/logger.zig @@ -232,7 +232,7 @@ pub const Data = struct { if (rest_of_line.len > 0) { var end_of_segment: usize = 1; - var iter = strings.CodepointIterator{ .bytes = rest_of_line, .i = 1 }; + var iter = strings.CodepointIterator.initOffset(rest_of_line, 1); // extremely naive: we should really use IsIdentifierContinue || isIdentifierStart here // highlight until we reach the next matching @@ -878,6 +878,11 @@ pub const Source = struct { line_count: usize, }; + pub fn initEmptyFile(filepath: string) Source { + const path = fs.Path.init(filepath); + return Source{ .path = path, .key_path = path, .index = 0, .contents = "" }; + } + pub fn initFile(file: fs.File, allocator: *std.mem.Allocator) !Source { var name = file.path.name; @@ -968,7 +973,7 @@ pub const Source = struct { pub fn initErrorPosition(self: *const Source, _offset: Loc) ErrorPosition { var prev_code_point: u21 = 0; - var offset: usize = std.math.min(if (_offset.start < 0) 0 else @intCast(usize, _offset.start), self.contents.len - 1); + var offset: usize = std.math.min(if (_offset.start < 0) 0 else @intCast(usize, _offset.start), @maximum(self.contents.len, 1) - 1); const contents = self.contents; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 93c16a24e..729a1edbf 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -727,7 +727,11 @@ inline fn _joinAbsStringBuf(comptime is_sentinel: bool, comptime ReturnType: typ return _cwd; } - if ((_platform == .loose or _platform == .posix) and parts.len == 1 and parts[0].len == 1 and parts[0][0] == std.fs.path.sep_posix) { + if ((comptime _platform == .loose or _platform == .posix) and + parts.len == 1 and + parts[0].len == 1 and + parts[0][0] == std.fs.path.sep_posix) + { return "/"; } diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 1434dc273..a29d9c9ca 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -1224,7 +1224,9 @@ pub const Resolver = struct { // directory path accidentally being interpreted as URL escapes. var esm_resolution = esmodule.resolve("/", esm.subpath, exports_map.root); - if ((esm_resolution.status == .Inexact or esm_resolution.status == .Exact) and strings.startsWith(esm_resolution.path, "/")) { + if ((esm_resolution.status == .Inexact or esm_resolution.status == .Exact) and + strings.startsWith(esm_resolution.path, "/")) + { const abs_esm_path: string = brk: { var parts = [_]string{ abs_package_path, diff --git a/src/router.zig b/src/router.zig index 38ddcc7a9..90ff23b30 100644 --- a/src/router.zig +++ b/src/router.zig @@ -15,6 +15,7 @@ const Options = @import("./options.zig"); const allocators = @import("./allocators.zig"); const URLPath = @import("./http/url_path.zig"); const PathnameScanner = @import("./query_string_map.zig").PathnameScanner; +const CodepointIterator = @import("./string_immutable.zig").CodepointIterator; const index_route_hash = @truncate(u32, std.hash.Wyhash.hash(0, "index")); const arbitrary_max_route = 4096; @@ -33,7 +34,7 @@ pub fn init( ) !Router { return Router{ .routes = RouteMap{ - .routes = Route.List{}, + .routes = RouteGroup.Root.initEmpty(), .index = null, .allocator = allocator, .config = config, @@ -102,113 +103,384 @@ const banned_dirs = [_]string{ "node_modules", }; -// This loads routes recursively, in depth-first order. -// it does not currently handle duplicate exact route matches. that's undefined behavior, for now. -pub fn loadRoutes( - this: *Router, - root_dir_info: *const DirInfo, - comptime ResolverType: type, - resolver: *ResolverType, - parent: u16, - comptime is_root: bool, -) anyerror!void { - var fs = &this.fs.fs; - if (root_dir_info.getEntriesConst()) |entries| { - var iter = entries.data.iterator(); - outer: while (iter.next()) |entry_ptr| { - const entry = entry_ptr.value; - if (entry.base()[0] == '.') { - continue :outer; - } +const RouteEntry = struct { + route: *Route, + hash: u32, - switch (entry.kind(fs)) { - .dir => { - inline for (banned_dirs) |banned_dir| { - if (strings.eqlComptime(entry.base(), comptime banned_dir)) { - continue :outer; - } - } + pub const List = std.MultiArrayList(RouteEntry); + + pub fn indexInList(hashes: []u32, hash: u32) ?u32 { + for (hashes) |hash_, i| { + if (hash_ == hash) return @truncate(u32, i); + } + + return null; + } +}; + +pub const RouteGroup = struct { + /// **Static child routes** + /// Each key's pointer starts at the offset of the pattern string + /// + /// When no more dynamic paramters exist for the route, it will live in this hash table. + /// + /// index routes live in the parent's `RouteGroup` and do not have `"index"` in the key + /// + /// `"edit"` -> `"pages/posts/[id]/edit.js"` + /// `"posts/all"` -> `"pages/posts/all.js"` + /// `"posts"` -> `"pages/posts/index.js"` or `"pages/posts.js"` + + static: std.StringArrayHashMapUnmanaged(*Route) = std.StringArrayHashMapUnmanaged(*Route){}, + child: ?*RouteGroup = null, + +/// **Dynamic Route** +/// +/// When it's the final pattern in the route and there was no index route, this route will match. Only matches when there is still text for a single segment. +/// +/// `posts/[id]` -> `"pages/posts/[id].js"` + + dynamic: ?*Route = null, + + /// **Catch all route** +/// +/// +/// +/// `posts/[id]` -> `"pages/posts/[id].js"` + catch_all: ?*Route = null, + catch_all_is_optional: bool = false, + + offset: u32 = 0, + + pub const zero = RouteGroup{}; + + pub fn isEmpty(this: *const RouteGroup) bool { + this.dy + return this.static.count() == 0 and this.child == null and this.index == null and this.dynamic == null and this.catch_all == null; + } + + pub fn init() RouteGroup { + return RouteGroup{ + .index = null, + .static = std.StringArrayHashMapUnmanaged(*Route){}, + }; + } - var abs_parts = [_]string{ entry.dir, entry.base() }; - if (resolver.readDirInfoIgnoreError(this.fs.abs(&abs_parts))) |_dir_info| { - const dir_info: *const DirInfo = _dir_info; + pub fn insert(this: *RouteGroup, allocator: *std.mem.Allocator, routes: []*Route, offset: u32) u32 { + if (comptime isDebug) { + std.debug.assert(offset > 0); + std.debug.assert(this.offset == 0 or this.offset == offset); + } - var route: Route = Route.parse( - entry.base(), - Fs.PathName.init(entry.dir[this.config.dir.len..]).dirWithTrailingSlash(), - "", - entry, - ); + this.offset = offset; - route.parent = parent; + var i: usize = 0; + while (i < routes.len) { + var j: usize = i + 1; + defer i = j; + } + } - route.children.offset = @truncate(u16, this.routes.routes.len + 1); - try this.routes.routes.append(this.allocator, route); + pub const Root = struct { + all: []*Route = &[_]*Route{}, - // potential stack overflow! - try this.loadRoutes( - dir_info, - ResolverType, - resolver, - route.children.offset - 1, - false, - ); + /// completely static children of indefinite depth + /// `"blog/posts"` + /// `"dashboard"` + /// `"profiles"` + /// this is a fast path? + static: std.StringHashMap(*Route), - this.routes.routes.items(.children)[route.children.offset - 1].len = @truncate(u16, this.routes.routes.len) - route.children.offset; - } - }, + /// The root can only have one of these + /// These routes have at least one parameter somewhere + children: ?RouteGroup = null, - .file => { - const extname = std.fs.path.extension(entry.base()); - // exclude "." or "" - if (extname.len < 2) continue; - - for (this.config.extensions) |_extname| { - if (strings.eql(extname[1..], _extname)) { - var route = Route.parse( - entry.base(), - // we extend the pointer length by one to get it's slash - entry.dir.ptr[this.config.dir.len..entry.dir.len], - extname, - entry, - ); - route.parent = parent; + /// Corresponds to "index.js" on the filesystem + index: ?*Route = null, - if (comptime is_root) { - if (strings.eqlComptime(route.name, "index")) { - this.routes.index = @truncate(u32, this.routes.routes.len); - } + pub fn initEmpty() Root { + return Root{ + .static = std.StringHashMap(*Route).init(default_allocator), + .children = std.StringArrayHashMap(RouteGroup).init(default_allocator), + }; + } + + pub fn insert(this: *Root, allocator: *std.mem.Allocator, log: *Logger.Log, children: []*Route) void { + var i: u32 = 0; + var end = @intCast(u32, children.len); + var j: u32 = 0; + while (i < children.len) { + var route = children[i]; + + if (comptime isDebug) { + std.debug.assert(route.param_count > 0); + } + + const first_pattern = Pattern.init(route.name, 0) catch unreachable; + + // Since routes are sorted by [ appearing last + // and we make all static routes fit into a separate hash table first + // we can assume that if the pattern is the last one, it's a dynamic route of some kind + if (first_pattern.isEnd(route.name)) { + switch (first_pattern.value) { + .static => unreachable, // thats a bug + .dynamic => { + if (this.dynamic != null) { + log.addErrorFmt(null, Logger.Loc.Empty, allocator, + \\Multiple dynamic routes can't be on the root route. Rename either: + \\ + \\ {s} + \\ {s} + \\ + , .{ + route.abs_path.str(), this.dynamic.?.abs_path.str(), + }); } - try this.routes.routes.append( - this.allocator, - route, - ); - } + this.dynamic = route; + }, + .optional_catch_all, .catch_all => { + if (this.fallback != null) { + log.addErrorFmt(null, Logger.Loc.Empty, allocator, + \\Multiple catch-all routes can't be on the root route. Rename either: + \\ + \\ {s} + \\ {s} + \\ + , .{ + route.abs_path.str(), this.fallback.?.abs_path.str(), + }); + } + + this.fallback = route; + }, + } + + return; + } + + j = i + 1; + defer i = j; + + if (j >= children.len) { + var entry = this.children.getOrPut(hashed_string.str()) catch unreachable; + if (!entry.found_existing) { + entry.value_ptr.* = RouteGroup.init(allocator); } + + _ = entry.value_ptr.insert(routes[@maximum(@as(usize, i), 1) - 1 ..], 0); + return; + } + + var second_route = children[j]; + var second_pattern = Pattern.init(second_route.name, 0) catch unreachable; + var prev_pattern = second_pattern; + while (j < children.len and first_pattern.eql(second_pattern)) : (j += 1) { + prev_pattern = second_pattern; + second_route = children[j]; + } + + if (this.children == null) { + this.children = RouteGroup.init(allocator); + } + + this.children.?.insert(routes[i..j], first_pattern.len); + } + } + }; +}; + +const RouteLoader = struct { + allocator: *std.mem.Allocator, + fs: *FileSystem, + config: Options.RouteConfig, + + list: RouteEntry.List, + log: *Logger.Log, + index: ?*Route = null, + static_list: std.StringHashMap(*Route), + + all_routes: std.ArrayListUnmanaged(*Route), + + pub fn appendRoute(this: *RouteLoader, route: Route) void { + // /index.js + if (route.full_hash == index_route_hash) { + var new_route = this.allocator.create(Route) catch unreachable; + this.index = new_route; + this.all_routes.append(this.allocator, new_route) catch unreachable; + return; + } + + // static route + if (route.param_count == 0) { + var entry = this.static_list.getOrPut(route.name) catch unreachable; + + if (entry.found_existing) { + const source = Logger.Source.initEmptyFile(route.abs_path.slice()); + this.log.addErrorFmt( + &source, + Logger.Loc.Empty, + this.allocator, + "Route {s} is already defined by {s}", + .{ route.name, entry.value_ptr.*.abs_path.slice() }, + ) catch unreachable; + return; + } + + var new_route = this.allocator.create(Route) catch unreachable; + new_route.* = route; + entry.value_ptr.* = new_route; + this.all_routes.append(this.allocator, new_route) catch unreachable; + return; + } + + // dynamic-ish + { + // This becomes a dead pointer at the end + var slice = this.list.slice(); + + const hashes = slice.items(.hash); + if (std.mem.indexOfScalar(u32, hashes, route.full_hash)) |i| { + const routes = slice.items(.route); + + if (comptime isDebug) { + std.debug.assert(strings.eql(routes[i].name, route.name)); + } + + const source = Logger.Source.initEmptyFile(route.abs_path.slice()); + this.log.addErrorFmt( + &source, + Logger.Loc.Empty, + this.allocator, + "Route {s} is already defined by {s}", + .{ route.name, routes[i].abs_path.slice() }, + ) catch unreachable; + return; + } + } + + { + var new_route = this.allocator.create(Route) catch unreachable; + new_route.* = route; + + this.list.append( + this.allocator, + .{ + .hash = route.full_hash, + .route = new_route, }, + ) catch unreachable; + this.all_routes.append(this.allocator, new_route) catch unreachable; + } + } + + pub fn loadAll(allocator: *std.mem.Allocator, config: Options.RouteConfig, log: *Logger.Log, comptime ResolverType: type, resolver: *ResolverType, root_dir_info: *const DirInfo) RouteGroup.Root { + var this = RouteLoader{ + .allocator = allocator, + .log = log, + .fs = resolver.fs, + .config = config, + .list = .{}, + .static_list = std.StringHashMap(*Route).init(allocator), + .all_routes = .{}, + }; + this.load(ResolverType, resolver, root_dir_info); + if (this.list.len + this.static_list.count() == 0) return RouteGroup.Root.initEmpty(); + + var root = RouteGroup.Root{ + .all = this.all_routes.toOwnedSlice(allocator), + .index = this.index, + .static = this.static_list, + .children = std.StringArrayHashMap(RouteGroup).init(this.allocator), + }; + + var list = this.list.toOwnedSlice(); + + var routes = list.items(.route); + + if (routes.len > 0) { + std.sort.sort(*Route, routes, Route.Sorter{}, Route.Sorter.sortByName); + for (routes) |route| { + Output.prettyErrorln("\nName: <b>{s}<r>", .{route.name}); } + // root.insert(allocator, log, routes); } + + // return root; + return root; } - if (comptime isDebug) { - if (comptime is_root) { - var i: usize = 0; - Output.prettyErrorln("Routes (last segment only):", .{}); - while (i < this.routes.routes.len) : (i += 1) { - const route = this.routes.routes.get(i); + pub fn load(this: *RouteLoader, comptime ResolverType: type, resolver: *ResolverType, root_dir_info: *const DirInfo) void { + var fs = this.fs; - Output.prettyErrorln(" {s}: {s}", .{ route.name, route.path }); + if (root_dir_info.getEntriesConst()) |entries| { + var iter = entries.data.iterator(); + outer: while (iter.next()) |entry_ptr| { + const entry = entry_ptr.value; + if (entry.base()[0] == '.') { + continue :outer; + } + + switch (entry.kind(&fs.fs)) { + .dir => { + inline for (banned_dirs) |banned_dir| { + if (strings.eqlComptime(entry.base(), comptime banned_dir)) { + continue :outer; + } + } + + var abs_parts = [_]string{ entry.dir, entry.base() }; + if (resolver.readDirInfoIgnoreError(fs.abs(&abs_parts))) |_dir_info| { + const dir_info: *const DirInfo = _dir_info; + + this.load( + ResolverType, + resolver, + dir_info, + ); + } + }, + + .file => { + const extname = std.fs.path.extension(entry.base()); + // exclude "." or "" + if (extname.len < 2) continue; + + for (this.config.extensions) |_extname| { + if (strings.eql(extname[1..], _extname)) { + if (Route.parse( + entry.base_lowercase(), + // we extend the pointer length by one to get it's slash + entry.dir.ptr[this.config.dir.len..entry.dir.len], + extname, + entry, + this.log, + this.allocator, + )) |route| { + this.appendRoute(route); + } + break; + } + } + }, + } } - Output.prettyErrorln(" {d} routes\n", .{this.routes.routes.len}); - Output.flush(); } } -} +}; + +// This loads routes recursively, in depth-first order. +// it does not currently handle duplicate exact route matches. that's undefined behavior, for now. +pub fn loadRoutes( + this: *Router, + root_dir_info: *const DirInfo, + comptime ResolverType: type, + resolver: *ResolverType, + comptime is_root: bool, +) anyerror!void {} pub const TinyPtr = packed struct { - offset: u16 = 0, - len: u16 = 0, + offset: u32 = 0, + len: u32 = 0, pub inline fn str(this: TinyPtr, slice: string) string { return if (this.len > 0) slice[this.offset .. this.offset + this.len] else ""; @@ -216,6 +488,10 @@ pub const TinyPtr = packed struct { pub inline fn toStringPointer(this: TinyPtr) Api.StringPointer { return Api.StringPointer{ .offset = this.offset, .length = this.len }; } + + pub inline fn eql(a: TinyPtr, b: TinyPtr) bool { + return @bitCast(u64, a) == @bitCast(u64, b); + } }; pub const Param = struct { @@ -227,50 +503,137 @@ pub const Param = struct { }; pub const Route = struct { - part: RoutePart, name: string, - path: string, - hash: u32, - children: Ptr = Ptr{}, - parent: u16 = top_level_parent, entry: *Fs.FileSystem.Entry, - full_hash: u32, + param_count: u16, + abs_path: PathString, - pub const top_level_parent = std.math.maxInt(u16); - - pub const List = std.MultiArrayList(Route); pub const Ptr = TinyPtr; - pub fn parse(base: string, dir: string, extname: string, entry: *Fs.FileSystem.Entry) Route { - const ensure_slash = if (dir.len > 0 and dir[dir.len - 1] != '/') "/" else ""; + pub const index_route_name: string = "index"; + var route_file_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + + pub const Sorter = struct { + const sort_table: [std.math.maxInt(u8)]u8 = brk: { + var table: [std.math.maxInt(u8)]u8 = undefined; + var i: u16 = 0; + while (i < @as(u16, table.len)) { + table[i] = @intCast(u8, i); + i += 1; + } + // move dynamic routes to the bottom + table['['] = 252; + table[']'] = 253; + // of each segment + table['/'] = 1; + break :brk table; + }; + + pub fn sortByNameString(ctx: @This(), lhs: string, rhs: string) bool { + const math = std.math; + + const n = @minimum(lhs.len, rhs.len); + var i: usize = 0; + while (i < n) : (i += 1) { + switch (math.order(sort_table[lhs[i]], sort_table[rhs[i]])) { + .eq => continue, + .lt => return true, + .gt => return false, + } + } + return math.order(lhs.len, rhs.len) == .lt; + } + + pub fn sortByName(ctx: @This(), a: *Route, b: *Route) bool { + return @call(.{ .modifier = .always_inline }, sortByNameString, .{ ctx, a.name, b.name }); + } + }; + + pub fn parse( + base_: string, + dir: string, + extname: string, + entry: *Fs.FileSystem.Entry, + log: *Logger.Log, + allocator: *std.mem.Allocator, + ) ?Route { + var abs_path_str: string = if (entry.abs_path.isEmpty()) + "" + else + entry.abs_path.slice(); + + var base = base_[0 .. base_.len - extname.len]; - var parts = [3]string{ dir, ensure_slash, base }; - // this isn't really absolute, it's relative to the pages dir - const absolute = Fs.FileSystem.instance.join(&parts); - const name = base[0 .. base.len - extname.len]; - const start_index: usize = if (absolute[0] == '/') 1 else 0; - var hash_path = absolute[start_index .. absolute.len - extname.len]; + if (strings.eql(base, "index")) { + base = ""; + } + + var route_name: string = std.mem.trimRight(u8, dir, "/"); + + var name: string = brk: { + if (route_name.len == 0) break :brk base; + _ = strings.copyLowercase(route_name, &route_file_buf); + route_file_buf[route_name.len] = '/'; + std.mem.copy(u8, route_file_buf[route_name.len + 1 ..], base); + break :brk route_file_buf[0 .. route_name.len + 1 + base.len]; + }; + + while (name.len > 0 and name[name.len - 1] == '/') { + name = name[0 .. name.len - 1]; + } + + name = std.mem.trimLeft(u8, name, "/"); + + var param_count: u16 = 0; + + if (name.len > 0) { + param_count = Pattern.validate( + name, + allocator, + log, + ) orelse return null; + name = FileSystem.DirnameStore.instance.append(@TypeOf(name), name) catch unreachable; + } else { + name = Route.index_route_name; + } + + if (abs_path_str.len == 0) { + var file: std.fs.File = undefined; + var needs_close = false; + defer if (needs_close) file.close(); + if (entry.cache.fd != 0) { + file = std.fs.File{ .handle = entry.cache.fd }; + } else { + var parts = [_]string{ entry.dir, entry.base() }; + abs_path_str = FileSystem.instance.absBuf(&parts, &route_file_buf); + route_file_buf[abs_path_str.len] = 0; + var buf = route_file_buf[0..abs_path_str.len :0]; + file = std.fs.openFileAbsoluteZ(buf, .{ .read = true }) catch |err| { + log.addErrorFmt(null, Logger.Loc.Empty, allocator, "{s} opening route: {s}", .{ @errorName(err), abs_path_str }) catch unreachable; + return null; + }; + FileSystem.setMaxFd(file.handle); + + needs_close = FileSystem.instance.fs.needToCloseFiles(); + if (!needs_close) entry.cache.fd = file.handle; + } + + var _abs = std.os.getFdPath(file.handle, &route_file_buf) catch |err| { + log.addErrorFmt(null, Logger.Loc.Empty, allocator, "{s} resolving route: {s}", .{ @errorName(err), abs_path_str }) catch unreachable; + return null; + }; + + abs_path_str = FileSystem.DirnameStore.instance.append(@TypeOf(_abs), _abs) catch unreachable; + entry.abs_path = PathString.init(abs_path_str); + } return Route{ .name = name, - .path = base, .entry = entry, - .hash = @truncate( - u32, - std.hash.Wyhash.hash( - 0, - name, - ), - ), - .full_hash = @truncate( - u32, - std.hash.Wyhash.hash( - 0, - hash_path, - ), - ), - .part = RoutePart.parse(name), + .full_hash = @truncate(u32, std.hash.Wyhash.hash(0, abs_path_str)), + .param_count = param_count, + .abs_path = entry.abs_path, }; } }; @@ -290,7 +653,7 @@ pub const Route = struct { // - pages/foo/hello-[bar] // - pages/foo/[bar]-foo pub const RouteMap = struct { - routes: Route.List, + routes: RouteGroup.Root, index: ?u32, allocator: *std.mem.Allocator, config: Options.RouteConfig, @@ -758,6 +1121,279 @@ pub const Match = struct { } }; +const Pattern = struct { + value: Value, + len: u32 = 0, + + // pub fn match(path: string, name: string, params: *para) bool { + // var offset: u32 = 0; + // var path_i: u32 = 0; + // while (offset < name.len) { + // var pattern = Pattern.init(name, 0) catch unreachable; + // var path_ = path[path_i..]; + + // switch (pattern.value) { + // .static => |str| { + // if (!strings.eql(str, path_[0..str.len])) { + // return false; + // } + + // path_ = path_[str.len..]; + // offset = pattern.len; + // }, + // } + // } + + // return true; + // } + + /// Validate a Route pattern, returning the number of route parameters. + /// `null` means invalid. Error messages are logged. + /// That way, we can provide a list of all invalid routes rather than failing the first time. + pub fn validate(input: string, allocator: *std.mem.Allocator, log: *Logger.Log) ?u16 { + if (CodepointIterator.needsUTF8Decoding(input)) { + const source = Logger.Source.initEmptyFile(input); + log.addErrorFmt( + &source, + Logger.Loc.Empty, + allocator, + "Route name must be plaintext", + .{}, + ) catch unreachable; + return null; + } + + var count: u16 = 0; + var offset: u32 = 0; + std.debug.assert(input.len > 0); + + const end = @truncate(u32, input.len - 1); + while (offset < end) { + const pattern: Pattern = Pattern.initUnhashed(input, offset) catch |err| { + const source = Logger.Source.initEmptyFile(input); + switch (err) { + error.CatchAllMustBeAtTheEnd => { + log.addErrorFmt( + &source, + Logger.Loc.Empty, + allocator, + "Catch-all route must be at the end of the path", + .{}, + ) catch unreachable; + }, + error.InvalidCatchAllRoute => { + log.addErrorFmt( + &source, + Logger.Loc.Empty, + allocator, + "Invalid catch-all route, e.g. should be [...param]", + .{}, + ) catch unreachable; + }, + error.InvalidOptionalCatchAllRoute => { + log.addErrorFmt( + &source, + Logger.Loc.Empty, + allocator, + "Invalid optional catch-all route, e.g. should be [[...param]]", + .{}, + ) catch unreachable; + }, + error.InvalidRoutePattern => { + log.addErrorFmt( + &source, + Logger.Loc.Empty, + allocator, + "Invalid dynamic route", + .{}, + ) catch unreachable; + }, + error.MissingParamName => { + log.addErrorFmt( + &source, + Logger.Loc.Empty, + allocator, + "Route is missing a parameter name, e.g. [param]", + .{}, + ) catch unreachable; + }, + error.PatternMissingClosingBracket => { + log.addErrorFmt( + &source, + Logger.Loc.Empty, + allocator, + "Route is missing a closing bracket]", + .{}, + ) catch unreachable; + }, + } + return null; + }; + offset = pattern.len; + count += @intCast(u16, @boolToInt(@enumToInt(@as(Pattern.Tag, pattern.value)) > @enumToInt(Pattern.Tag.static))); + } + + return count; + } + + pub fn eql(a: Pattern, b: Pattern) bool { + return a.len == b.len and a.value.eql(b.value); + } + + pub const PatternParseError = error{ + CatchAllMustBeAtTheEnd, + InvalidCatchAllRoute, + InvalidOptionalCatchAllRoute, + InvalidRoutePattern, + MissingParamName, + PatternMissingClosingBracket, + }; + + pub fn init(input: string, offset_: u32) PatternParseError!Pattern { + return initMaybeHash(input, offset_, true); + } + + pub fn isEnd(this: Pattern, input: string) bool { + return @as(usize, this.len) >= input.len; + } + + pub fn initUnhashed(input: string, offset_: u32) PatternParseError!Pattern { + return initMaybeHash(input, offset_, false); + } + + inline fn initMaybeHash(input: string, offset_: u32, comptime do_hash: bool) PatternParseError!Pattern { + const initHashedString = if (comptime do_hash) HashedString.init else HashedString.initNoHash; + + var offset: u32 = offset_; + + while (input.len > @as(usize, offset) and input[offset] == '/') { + offset += 1; + } + + if (input.len == 0 or input.len <= @as(usize, offset)) return Pattern{ + .value = .{ .static = HashedString.empty }, + .len = @truncate(u32, @minimum(input.len, @as(usize, offset))), + }; + + var i: u32 = offset; + + var tag = Tag.static; + const end = @intCast(u32, input.len - 1); + + if (offset == end) return Pattern{ .len = offset, .value = .{ .static = HashedString.empty } }; + + while (i <= end) : (i += 1) { + switch (input[i]) { + '/' => { + return Pattern{ .len = i, .value = .{ .static = initHashedString(input[offset..i]) } }; + }, + '[' => { + if (i > offset) { + return Pattern{ .len = i, .value = .{ .static = initHashedString(input[offset..i]) } }; + } + + tag = Tag.dynamic; + + var param = TinyPtr{}; + var catch_all_start = i; + + i += 1; + + param.offset = i; + + if (i >= end) return error.InvalidRoutePattern; + + switch (input[i]) { + '/', ']' => return error.MissingParamName, + '[' => { + tag = Tag.optional_catch_all; + + if (end < i + 4) { + return error.InvalidOptionalCatchAllRoute; + } + + const catch_all_dot_start = i; + if (!strings.eqlComptimeIgnoreLen(input[i..][0..3], "...")) return error.InvalidOptionalCatchAllRoute; + i += 4; + param.offset = i; + }, + '.' => { + tag = Tag.catch_all; + i += 1; + + if (end < i + 2) { + return error.InvalidCatchAllRoute; + } + + if (!strings.eqlComptimeIgnoreLen(input[i..][0..2], "..")) return error.InvalidCatchAllRoute; + i += 2; + + param.offset = i; + }, + else => {}, + } + + i += 1; + while (i <= end and input[i] != ']') : (i += 1) { + if (input[i] == '/') return error.InvalidRoutePattern; + } + + if (i > end) return error.PatternMissingClosingBracket; + + param.len = i - param.offset; + + i += 1; + + if (tag == Tag.optional_catch_all) { + i += 1; + + if (input[i] != ']') return error.PatternMissingClosingBracket; + } + + if (@enumToInt(tag) > @enumToInt(Tag.dynamic) and i <= end) return error.CatchAllMustBeAtTheEnd; + + return Pattern{ + .len = @minimum(end, i), + .value = switch (tag) { + .dynamic => .{ + .dynamic = param, + }, + .catch_all => .{ .catch_all = param }, + .optional_catch_all => .{ .optional_catch_all = param }, + else => unreachable, + }, + }; + }, + else => {}, + } + } + return Pattern{ .len = i, .value = .{ .static = HashedString.init(input[offset..i]) } }; + } + + pub const Tag = enum(u4) { + static = 0, + dynamic = 1, + catch_all = 2, + optional_catch_all = 3, + }; + + pub const Value = union(Tag) { + static: HashedString, + dynamic: TinyPtr, + catch_all: TinyPtr, + optional_catch_all: TinyPtr, + + pub fn eql(a: Value, b: Value) bool { + return @as(Tag, a) == @as(Tag, b) and switch (a) { + .static => HashedString.eql(a.static, b.static), + .dynamic => a.dynamic.eql(b.dynamic), + .catch_all => a.catch_all.eql(b.catch_all), + .optional_catch_all => a.optional_catch_all.eql(b.optional_catch_all), + }; + } + }; +}; + const FileSystem = Fs.FileSystem; const MockRequestContextType = struct { @@ -804,6 +1440,7 @@ fn makeTest(cwd_path: string, data: anytype) !void { const Data = @TypeOf(data); const fields: []const std.builtin.TypeInfo.StructField = comptime std.meta.fields(Data); inline for (fields) |field| { + @setEvalBranchQuota(9999); const value = @field(data, field.name); if (std.fs.path.dirname(field.name)) |dir| { @@ -818,10 +1455,11 @@ fn makeTest(cwd_path: string, data: anytype) !void { const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const expectEqualStrings = std.testing.expectEqualStrings; +const expectStr = std.testing.expectEqualStrings; const Logger = @import("./logger.zig"); pub const Test = struct { - pub fn make(comptime testName: string, data: anytype) !Router { + pub fn makeRoot(comptime testName: string, data: anytype) !RouteGroup.Root { try makeTest(testName, data); const JSAst = @import("./js_ast.zig"); JSAst.Expr.Data.Store.create(default_allocator); @@ -853,6 +1491,7 @@ pub const Test = struct { .loaders = undefined, .define = undefined, .log = &logger, + .routes = router.config, .entry_points = &.{}, .out_extensions = std.StringHashMap(string).init(default_allocator), .transform_options = std.mem.zeroes(Api.TransformOptions), @@ -870,14 +1509,23 @@ pub const Test = struct { var root_dir = (try resolver.readDirInfo(pages_dir)).?; var entries = root_dir.getEntries().?; - try router.loadRoutes(root_dir, Resolver, &resolver, 0, true); - var entry_points = try router.getEntryPoints(default_allocator); + return RouteLoader.loadAll(default_allocator, opts.routes, &logger, Resolver, &resolver, root_dir); + // try router.loadRoutes(root_dir, Resolver, &resolver, 0, true); + // var entry_points = try router.getEntryPoints(default_allocator); - try expectEqual(std.meta.fieldNames(@TypeOf(data)).len, entry_points.len); - return router; + // try expectEqual(std.meta.fieldNames(@TypeOf(data)).len, entry_points.len); + // return router; } }; +test "Route Loader" { + var server = MockServer{}; + var ctx = MockRequestContextType{ + .url = try URLPath.parse("/hi"), + }; + var router = try Test.makeRoot("routes-basic", github_api_routes_list); +} + test "Routes basic" { var server = MockServer{}; var ctx = MockRequestContextType{ @@ -889,7 +1537,7 @@ test "Routes basic" { .@"pages/blog/hi.js" = "//blog/hi", }); try router.match(&server, MockRequestContextType, &ctx); - try expectEqualStrings(ctx.matched_route.?.name, "/hi"); + try expectEqualStrings(ctx.matched_route.?.name, "hi"); ctx = MockRequestContextType{ .url = try URLPath.parse("/"), @@ -903,7 +1551,7 @@ test "Routes basic" { }; try router.match(&server, MockRequestContextType, &ctx); - try expectEqualStrings(ctx.matched_route.?.name, "/blog/hi"); + try expectEqualStrings(ctx.matched_route.?.name, "blog/hi"); ctx = MockRequestContextType{ .url = try URLPath.parse("/blog/hey"), @@ -941,7 +1589,7 @@ test "Dynamic routes" { }); try router.match(&server, MockRequestContextType, &ctx); - try expectEqualStrings(ctx.matched_route.?.name, "/blog/hi"); + try expectEqualStrings(ctx.matched_route.?.name, "blog/hi"); var params = ctx.matched_route.?.paramsIterator(); try expect(params.next() == null); @@ -963,3 +1611,324 @@ test "Dynamic routes" { // try router.match(&server, MockRequestContextType, &ctx); // try expectEqualStrings(ctx.matched_route.name, "index"); } + +test "Pattern" { + const pattern = "[dynamic]/static/[dynamic2]/[...catch_all]"; + + const dynamic = try Pattern.init(pattern, 0); + try expectStr(@tagName(dynamic.value), "dynamic"); + const static = try Pattern.init(pattern, dynamic.len); + try expectStr(@tagName(static.value), "static"); + const dynamic2 = try Pattern.init(pattern, static.len); + try expectStr(@tagName(dynamic2.value), "dynamic"); + const static2 = try Pattern.init(pattern, dynamic2.len); + try expectStr(@tagName(static2.value), "static"); + const catch_all = try Pattern.init(pattern, static2.len); + try expectStr(@tagName(catch_all.value), "catch_all"); + + try expectStr(dynamic.value.dynamic.str(pattern), "dynamic"); + try expectStr(static.value.static, "/static/"); + try expectStr(dynamic2.value.dynamic.str(pattern), "dynamic2"); + try expectStr(static2.value.static, "/"); + try expectStr(catch_all.value.catch_all.str(pattern), "catch_all"); +} + +const github_api_routes_list = .{ + .@"pages/[...catch-all-at-root].js" = "//pages/[...catch-all-at-root].js", + .@"pages/index.js" = "//pages/index.js", + .@"pages/app.js" = "//pages/app.js", + .@"pages/app/installations.js" = "//pages/app/installations.js", + .@"pages/app/installations/[installation_id].js" = "//pages/app/installations/[installation_id].js", + .@"pages/apps/[app_slug].js" = "//pages/apps/[app_slug].js", + .@"pages/codes_of_conduct.js" = "//pages/codes_of_conduct.js", + .@"pages/codes_of_conduct/[key].js" = "//pages/codes_of_conduct/[key].js", + .@"pages/emojis.js" = "//pages/emojis.js", + .@"pages/events.js" = "//pages/events.js", + .@"pages/feeds.js" = "//pages/feeds.js", + .@"pages/gitignore/templates.js" = "//pages/gitignore/templates.js", + .@"pages/gitignore/templates/[name].js" = "//pages/gitignore/templates/[name].js", + .@"pages/installation/repositories.js" = "//pages/installation/repositories.js", + .@"pages/licenses.js" = "//pages/licenses.js", + .@"pages/licenses/[license].js" = "//pages/licenses/[license].js", + .@"pages/meta.js" = "//pages/meta.js", + .@"pages/networks/[owner]/[repo]/events.js" = "//pages/networks/[owner]/[repo]/events.js", + .@"pages/octocat.js" = "//pages/octocat.js", + .@"pages/organizations.js" = "//pages/organizations.js", + .@"pages/orgs/[org]/index.js" = "//pages/orgs/[org].js", + .@"pages/orgs/[org]/actions/permissions.js" = "//pages/orgs/[org]/actions/permissions.js", + .@"pages/orgs/[org]/actions/permissions/repositories.js" = "//pages/orgs/[org]/actions/permissions/repositories.js", + .@"pages/orgs/[org]/actions/permissions/selected-actions.js" = "//pages/orgs/[org]/actions/permissions/selected-actions.js", + .@"pages/orgs/[org]/actions/runner-groups.js" = "//pages/orgs/[org]/actions/runner-groups.js", + .@"pages/orgs/[org]/actions/runner-groups/[runner_group_id].js" = "//pages/orgs/[org]/actions/runner-groups/[runner_group_id].js", + .@"pages/orgs/[org]/actions/runner-groups/[runner_group_id]/repositories.js" = "//pages/orgs/[org]/actions/runner-groups/[runner_group_id]/repositories.js", + .@"pages/orgs/[org]/actions/runner-groups/[runner_group_id]/runners.js" = "//pages/orgs/[org]/actions/runner-groups/[runner_group_id]/runners.js", + .@"pages/orgs/[org]/actions/runners.js" = "//pages/orgs/[org]/actions/runners.js", + .@"pages/orgs/[org]/actions/runners/[runner_id].js" = "//pages/orgs/[org]/actions/runners/[runner_id].js", + .@"pages/orgs/[org]/actions/runners/downloads.js" = "//pages/orgs/[org]/actions/runners/downloads.js", + .@"pages/orgs/[org]/actions/secrets.js" = "//pages/orgs/[org]/actions/secrets.js", + .@"pages/orgs/[org]/actions/secrets/[secret_name].js" = "//pages/orgs/[org]/actions/secrets/[secret_name].js", + .@"pages/orgs/[org]/actions/secrets/[secret_name]/repositories.js" = "//pages/orgs/[org]/actions/secrets/[secret_name]/repositories.js", + .@"pages/orgs/[org]/actions/secrets/public-key.js" = "//pages/orgs/[org]/actions/secrets/public-key.js", + .@"pages/orgs/[org]/audit-log.js" = "//pages/orgs/[org]/audit-log.js", + .@"pages/orgs/[org]/blocks.js" = "//pages/orgs/[org]/blocks.js", + .@"pages/orgs/[org]/blocks/[username].js" = "//pages/orgs/[org]/blocks/[username].js", + .@"pages/orgs/[org]/credential-authorizations.js" = "//pages/orgs/[org]/credential-authorizations.js", + .@"pages/orgs/[org]/events.js" = "//pages/orgs/[org]/events.js", + .@"pages/orgs/[org]/external-group/[group_id].js" = "//pages/orgs/[org]/external-group/[group_id].js", + .@"pages/orgs/[org]/external-groups.js" = "//pages/orgs/[org]/external-groups.js", + .@"pages/orgs/[org]/failed_invitations.js" = "//pages/orgs/[org]/failed_invitations.js", + .@"pages/orgs/[org]/hooks.js" = "//pages/orgs/[org]/hooks.js", + .@"pages/orgs/[org]/hooks/[hook_id].js" = "//pages/orgs/[org]/hooks/[hook_id].js", + .@"pages/orgs/[org]/hooks/[hook_id]/config.js" = "//pages/orgs/[org]/hooks/[hook_id]/config.js", + .@"pages/orgs/[org]/hooks/[hook_id]/deliveries.js" = "//pages/orgs/[org]/hooks/[hook_id]/deliveries.js", + .@"pages/orgs/[org]/hooks/[hook_id]/deliveries/[delivery_id].js" = "//pages/orgs/[org]/hooks/[hook_id]/deliveries/[delivery_id].js", + .@"pages/orgs/[org]/installations.js" = "//pages/orgs/[org]/installations.js", + .@"pages/orgs/[org]/interaction-limits.js" = "//pages/orgs/[org]/interaction-limits.js", + .@"pages/orgs/[org]/invitations.js" = "//pages/orgs/[org]/invitations.js", + .@"pages/orgs/[org]/invitations/[invitation_id]/teams.js" = "//pages/orgs/[org]/invitations/[invitation_id]/teams.js", + .@"pages/orgs/[org]/members.js" = "//pages/orgs/[org]/members.js", + .@"pages/orgs/[org]/members/[username].js" = "//pages/orgs/[org]/members/[username].js", + .@"pages/orgs/[org]/memberships/[username].js" = "//pages/orgs/[org]/memberships/[username].js", + .@"pages/orgs/[org]/outside_collaborators.js" = "//pages/orgs/[org]/outside_collaborators.js", + .@"pages/orgs/[org]/projects.js" = "//pages/orgs/[org]/projects.js", + .@"pages/orgs/[org]/public_members.js" = "//pages/orgs/[org]/public_members.js", + .@"pages/orgs/[org]/public_members/[username].js" = "//pages/orgs/[org]/public_members/[username].js", + .@"pages/orgs/[org]/repos.js" = "//pages/orgs/[org]/repos.js", + .@"pages/orgs/[org]/secret-scanning/alerts.js" = "//pages/orgs/[org]/secret-scanning/alerts.js", + .@"pages/orgs/[org]/team-sync/groups.js" = "//pages/orgs/[org]/team-sync/groups.js", + .@"pages/orgs/[org]/teams.js" = "//pages/orgs/[org]/teams.js", + .@"pages/orgs/[org]/teams/[team_slug].js" = "//pages/orgs/[org]/teams/[team_slug].js", + .@"pages/orgs/[org]/teams/[team_slug]/discussions.js" = "//pages/orgs/[org]/teams/[team_slug]/discussions.js", + .@"pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number].js" = "//pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number].js", + .@"pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number]/comments.js" = "//pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number]/comments.js", + .@"pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number]/comments/[comment_number].js" = "//pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number]/comments/[comment_number].js", + .@"pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number]/comments/[comment_number]/reactions.js" = "//pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number]/comments/[comment_number]/reactions.js", + .@"pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number]/reactions.js" = "//pages/orgs/[org]/teams/[team_slug]/discussions/[discussion_number]/reactions.js", + .@"pages/orgs/[org]/teams/[team_slug]/invitations.js" = "//pages/orgs/[org]/teams/[team_slug]/invitations.js", + .@"pages/orgs/[org]/teams/[team_slug]/members.js" = "//pages/orgs/[org]/teams/[team_slug]/members.js", + .@"pages/orgs/[org]/teams/[team_slug]/memberships/[username].js" = "//pages/orgs/[org]/teams/[team_slug]/memberships/[username].js", + .@"pages/orgs/[org]/teams/[team_slug]/projects.js" = "//pages/orgs/[org]/teams/[team_slug]/projects.js", + .@"pages/orgs/[org]/teams/[team_slug]/projects/[project_id].js" = "//pages/orgs/[org]/teams/[team_slug]/projects/[project_id].js", + .@"pages/orgs/[org]/teams/[team_slug]/repos.js" = "//pages/orgs/[org]/teams/[team_slug]/repos.js", + .@"pages/orgs/[org]/teams/[team_slug]/repos/[owner]/[repo].js" = "//pages/orgs/[org]/teams/[team_slug]/repos/[owner]/[repo].js", + .@"pages/orgs/[org]/teams/[team_slug]/teams.js" = "//pages/orgs/[org]/teams/[team_slug]/teams.js", + .@"pages/projects/[project_id].js" = "//pages/projects/[project_id].js", + .@"pages/projects/[project_id]/collaborators.js" = "//pages/projects/[project_id]/collaborators.js", + .@"pages/projects/[project_id]/collaborators/[username]/permission.js" = "//pages/projects/[project_id]/collaborators/[username]/permission.js", + .@"pages/projects/[project_id]/columns.js" = "//pages/projects/[project_id]/columns.js", + .@"pages/projects/columns/[column_id].js" = "//pages/projects/columns/[column_id].js", + .@"pages/projects/columns/[column_id]/cards.js" = "//pages/projects/columns/[column_id]/cards.js", + .@"pages/projects/columns/cards/[card_id].js" = "//pages/projects/columns/cards/[card_id].js", + .@"pages/rate_limit.js" = "//pages/rate_limit.js", + .@"pages/repos/[owner]/[repo].js" = "//pages/repos/[owner]/[repo].js", + .@"pages/repos/[owner]/[repo]/actions/artifacts.js" = "//pages/repos/[owner]/[repo]/actions/artifacts.js", + .@"pages/repos/[owner]/[repo]/actions/artifacts/[artifact_id].js" = "//pages/repos/[owner]/[repo]/actions/artifacts/[artifact_id].js", + .@"pages/repos/[owner]/[repo]/actions/artifacts/[artifact_id]/[archive_format].js" = "//pages/repos/[owner]/[repo]/actions/artifacts/[artifact_id]/[archive_format].js", + .@"pages/repos/[owner]/[repo]/actions/jobs/[job_id].js" = "//pages/repos/[owner]/[repo]/actions/jobs/[job_id].js", + .@"pages/repos/[owner]/[repo]/actions/jobs/[job_id]/logs.js" = "//pages/repos/[owner]/[repo]/actions/jobs/[job_id]/logs.js", + .@"pages/repos/[owner]/[repo]/actions/permissions.js" = "//pages/repos/[owner]/[repo]/actions/permissions.js", + .@"pages/repos/[owner]/[repo]/actions/permissions/selected-actions.js" = "//pages/repos/[owner]/[repo]/actions/permissions/selected-actions.js", + .@"pages/repos/[owner]/[repo]/actions/runners.js" = "//pages/repos/[owner]/[repo]/actions/runners.js", + .@"pages/repos/[owner]/[repo]/actions/runners/[runner_id].js" = "//pages/repos/[owner]/[repo]/actions/runners/[runner_id].js", + .@"pages/repos/[owner]/[repo]/actions/runners/downloads.js" = "//pages/repos/[owner]/[repo]/actions/runners/downloads.js", + .@"pages/repos/[owner]/[repo]/actions/runs.js" = "//pages/repos/[owner]/[repo]/actions/runs.js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id].js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id].js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id]/approvals.js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id]/approvals.js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id]/artifacts.js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id]/artifacts.js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id]/attempts/[attempt_number].js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id]/attempts/[attempt_number].js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id]/attempts/[attempt_number]/jobs.js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id]/attempts/[attempt_number]/jobs.js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id]/attempts/[attempt_number]/logs.js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id]/attempts/[attempt_number]/logs.js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id]/jobs.js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id]/jobs.js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id]/logs.js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id]/logs.js", + .@"pages/repos/[owner]/[repo]/actions/runs/[run_id]/pending_deployments.js" = "//pages/repos/[owner]/[repo]/actions/runs/[run_id]/pending_deployments.js", + .@"pages/repos/[owner]/[repo]/actions/secrets.js" = "//pages/repos/[owner]/[repo]/actions/secrets.js", + .@"pages/repos/[owner]/[repo]/actions/secrets/[secret_name].js" = "//pages/repos/[owner]/[repo]/actions/secrets/[secret_name].js", + .@"pages/repos/[owner]/[repo]/actions/secrets/public-key.js" = "//pages/repos/[owner]/[repo]/actions/secrets/public-key.js", + .@"pages/repos/[owner]/[repo]/actions/workflows.js" = "//pages/repos/[owner]/[repo]/actions/workflows.js", + .@"pages/repos/[owner]/[repo]/actions/workflows/[workflow_id].js" = "//pages/repos/[owner]/[repo]/actions/workflows/[workflow_id].js", + .@"pages/repos/[owner]/[repo]/actions/workflows/[workflow_id]/runs.js" = "//pages/repos/[owner]/[repo]/actions/workflows/[workflow_id]/runs.js", + .@"pages/repos/[owner]/[repo]/assignees.js" = "//pages/repos/[owner]/[repo]/assignees.js", + .@"pages/repos/[owner]/[repo]/assignees/[assignee].js" = "//pages/repos/[owner]/[repo]/assignees/[assignee].js", + .@"pages/repos/[owner]/[repo]/autolinks.js" = "//pages/repos/[owner]/[repo]/autolinks.js", + .@"pages/repos/[owner]/[repo]/autolinks/[autolink_id].js" = "//pages/repos/[owner]/[repo]/autolinks/[autolink_id].js", + .@"pages/repos/[owner]/[repo]/branches.js" = "//pages/repos/[owner]/[repo]/branches.js", + .@"pages/repos/[owner]/[repo]/branches/[branch].js" = "//pages/repos/[owner]/[repo]/branches/[branch].js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/enforce_admins.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/enforce_admins.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/required_pull_request_reviews.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/required_pull_request_reviews.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/required_signatures.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/required_signatures.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/required_status_checks.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/required_status_checks.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/required_status_checks/contexts.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/required_status_checks/contexts.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/restrictions.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/restrictions.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/restrictions/apps.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/restrictions/apps.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/restrictions/teams.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/restrictions/teams.js", + .@"pages/repos/[owner]/[repo]/branches/[branch]/protection/restrictions/users.js" = "//pages/repos/[owner]/[repo]/branches/[branch]/protection/restrictions/users.js", + .@"pages/repos/[owner]/[repo]/check-runs/[check_run_id].js" = "//pages/repos/[owner]/[repo]/check-runs/[check_run_id].js", + .@"pages/repos/[owner]/[repo]/check-runs/[check_run_id]/annotations.js" = "//pages/repos/[owner]/[repo]/check-runs/[check_run_id]/annotations.js", + .@"pages/repos/[owner]/[repo]/check-suites/[check_suite_id].js" = "//pages/repos/[owner]/[repo]/check-suites/[check_suite_id].js", + .@"pages/repos/[owner]/[repo]/check-suites/[check_suite_id]/check-runs.js" = "//pages/repos/[owner]/[repo]/check-suites/[check_suite_id]/check-runs.js", + .@"pages/repos/[owner]/[repo]/code-scanning/alerts.js" = "//pages/repos/[owner]/[repo]/code-scanning/alerts.js", + .@"pages/repos/[owner]/[repo]/code-scanning/alerts/[alert_number].js" = "//pages/repos/[owner]/[repo]/code-scanning/alerts/[alert_number].js", + .@"pages/repos/[owner]/[repo]/code-scanning/alerts/[alert_number]/instances.js" = "//pages/repos/[owner]/[repo]/code-scanning/alerts/[alert_number]/instances.js", + .@"pages/repos/[owner]/[repo]/code-scanning/analyses.js" = "//pages/repos/[owner]/[repo]/code-scanning/analyses.js", + .@"pages/repos/[owner]/[repo]/code-scanning/analyses/[analysis_id].js" = "//pages/repos/[owner]/[repo]/code-scanning/analyses/[analysis_id].js", + .@"pages/repos/[owner]/[repo]/code-scanning/sarifs/[sarif_id].js" = "//pages/repos/[owner]/[repo]/code-scanning/sarifs/[sarif_id].js", + .@"pages/repos/[owner]/[repo]/collaborators.js" = "//pages/repos/[owner]/[repo]/collaborators.js", + .@"pages/repos/[owner]/[repo]/collaborators/[username].js" = "//pages/repos/[owner]/[repo]/collaborators/[username].js", + .@"pages/repos/[owner]/[repo]/collaborators/[username]/permission.js" = "//pages/repos/[owner]/[repo]/collaborators/[username]/permission.js", + .@"pages/repos/[owner]/[repo]/comments.js" = "//pages/repos/[owner]/[repo]/comments.js", + .@"pages/repos/[owner]/[repo]/comments/[comment_id].js" = "//pages/repos/[owner]/[repo]/comments/[comment_id].js", + .@"pages/repos/[owner]/[repo]/comments/[comment_id]/reactions.js" = "//pages/repos/[owner]/[repo]/comments/[comment_id]/reactions.js", + .@"pages/repos/[owner]/[repo]/commits.js" = "//pages/repos/[owner]/[repo]/commits.js", + .@"pages/repos/[owner]/[repo]/commits/[commit_sha]/branches-where-head.js" = "//pages/repos/[owner]/[repo]/commits/[commit_sha]/branches-where-head.js", + .@"pages/repos/[owner]/[repo]/commits/[commit_sha]/comments.js" = "//pages/repos/[owner]/[repo]/commits/[commit_sha]/comments.js", + .@"pages/repos/[owner]/[repo]/commits/[commit_sha]/pulls.js" = "//pages/repos/[owner]/[repo]/commits/[commit_sha]/pulls.js", + .@"pages/repos/[owner]/[repo]/commits/[ref].js" = "//pages/repos/[owner]/[repo]/commits/[ref].js", + .@"pages/repos/[owner]/[repo]/commits/[ref]/check-runs.js" = "//pages/repos/[owner]/[repo]/commits/[ref]/check-runs.js", + .@"pages/repos/[owner]/[repo]/commits/[ref]/check-suites.js" = "//pages/repos/[owner]/[repo]/commits/[ref]/check-suites.js", + .@"pages/repos/[owner]/[repo]/commits/[ref]/status.js" = "//pages/repos/[owner]/[repo]/commits/[ref]/status.js", + .@"pages/repos/[owner]/[repo]/commits/[ref]/statuses.js" = "//pages/repos/[owner]/[repo]/commits/[ref]/statuses.js", + .@"pages/repos/[owner]/[repo]/community/profile.js" = "//pages/repos/[owner]/[repo]/community/profile.js", + .@"pages/repos/[owner]/[repo]/compare/[basehead].js" = "//pages/repos/[owner]/[repo]/compare/[basehead].js", + .@"pages/repos/[owner]/[repo]/contents/[path].js" = "//pages/repos/[owner]/[repo]/contents/[path].js", + .@"pages/repos/[owner]/[repo]/contributors.js" = "//pages/repos/[owner]/[repo]/contributors.js", + .@"pages/repos/[owner]/[repo]/deployments.js" = "//pages/repos/[owner]/[repo]/deployments.js", + .@"pages/repos/[owner]/[repo]/deployments/[deployment_id].js" = "//pages/repos/[owner]/[repo]/deployments/[deployment_id].js", + .@"pages/repos/[owner]/[repo]/deployments/[deployment_id]/statuses.js" = "//pages/repos/[owner]/[repo]/deployments/[deployment_id]/statuses.js", + .@"pages/repos/[owner]/[repo]/deployments/[deployment_id]/statuses/[status_id].js" = "//pages/repos/[owner]/[repo]/deployments/[deployment_id]/statuses/[status_id].js", + .@"pages/repos/[owner]/[repo]/environments.js" = "//pages/repos/[owner]/[repo]/environments.js", + .@"pages/repos/[owner]/[repo]/environments/[environment_name].js" = "//pages/repos/[owner]/[repo]/environments/[environment_name].js", + .@"pages/repos/[owner]/[repo]/events.js" = "//pages/repos/[owner]/[repo]/events.js", + .@"pages/repos/[owner]/[repo]/forks.js" = "//pages/repos/[owner]/[repo]/forks.js", + .@"pages/repos/[owner]/[repo]/git/blobs/[file_sha].js" = "//pages/repos/[owner]/[repo]/git/blobs/[file_sha].js", + .@"pages/repos/[owner]/[repo]/git/commits/[commit_sha].js" = "//pages/repos/[owner]/[repo]/git/commits/[commit_sha].js", + .@"pages/repos/[owner]/[repo]/git/matching-refs/[ref].js" = "//pages/repos/[owner]/[repo]/git/matching-refs/[ref].js", + .@"pages/repos/[owner]/[repo]/git/ref/[ref].js" = "//pages/repos/[owner]/[repo]/git/ref/[ref].js", + .@"pages/repos/[owner]/[repo]/git/tags/[tag_sha].js" = "//pages/repos/[owner]/[repo]/git/tags/[tag_sha].js", + .@"pages/repos/[owner]/[repo]/git/trees/[tree_sha].js" = "//pages/repos/[owner]/[repo]/git/trees/[tree_sha].js", + .@"pages/repos/[owner]/[repo]/hooks.js" = "//pages/repos/[owner]/[repo]/hooks.js", + .@"pages/repos/[owner]/[repo]/hooks/[hook_id].js" = "//pages/repos/[owner]/[repo]/hooks/[hook_id].js", + .@"pages/repos/[owner]/[repo]/hooks/[hook_id]/config.js" = "//pages/repos/[owner]/[repo]/hooks/[hook_id]/config.js", + .@"pages/repos/[owner]/[repo]/hooks/[hook_id]/deliveries.js" = "//pages/repos/[owner]/[repo]/hooks/[hook_id]/deliveries.js", + .@"pages/repos/[owner]/[repo]/hooks/[hook_id]/deliveries/[delivery_id].js" = "//pages/repos/[owner]/[repo]/hooks/[hook_id]/deliveries/[delivery_id].js", + .@"pages/repos/[owner]/[repo]/import.js" = "//pages/repos/[owner]/[repo]/import.js", + .@"pages/repos/[owner]/[repo]/import/authors.js" = "//pages/repos/[owner]/[repo]/import/authors.js", + .@"pages/repos/[owner]/[repo]/import/large_files.js" = "//pages/repos/[owner]/[repo]/import/large_files.js", + .@"pages/repos/[owner]/[repo]/interaction-limits.js" = "//pages/repos/[owner]/[repo]/interaction-limits.js", + .@"pages/repos/[owner]/[repo]/invitations.js" = "//pages/repos/[owner]/[repo]/invitations.js", + .@"pages/repos/[owner]/[repo]/issues.js" = "//pages/repos/[owner]/[repo]/issues.js", + .@"pages/repos/[owner]/[repo]/issues/[issue_number].js" = "//pages/repos/[owner]/[repo]/issues/[issue_number].js", + .@"pages/repos/[owner]/[repo]/issues/[issue_number]/comments.js" = "//pages/repos/[owner]/[repo]/issues/[issue_number]/comments.js", + .@"pages/repos/[owner]/[repo]/issues/[issue_number]/events.js" = "//pages/repos/[owner]/[repo]/issues/[issue_number]/events.js", + .@"pages/repos/[owner]/[repo]/issues/[issue_number]/labels.js" = "//pages/repos/[owner]/[repo]/issues/[issue_number]/labels.js", + .@"pages/repos/[owner]/[repo]/issues/[issue_number]/reactions.js" = "//pages/repos/[owner]/[repo]/issues/[issue_number]/reactions.js", + .@"pages/repos/[owner]/[repo]/issues/[issue_number]/timeline.js" = "//pages/repos/[owner]/[repo]/issues/[issue_number]/timeline.js", + .@"pages/repos/[owner]/[repo]/issues/comments.js" = "//pages/repos/[owner]/[repo]/issues/comments.js", + .@"pages/repos/[owner]/[repo]/issues/comments/[comment_id].js" = "//pages/repos/[owner]/[repo]/issues/comments/[comment_id].js", + .@"pages/repos/[owner]/[repo]/issues/comments/[comment_id]/reactions.js" = "//pages/repos/[owner]/[repo]/issues/comments/[comment_id]/reactions.js", + .@"pages/repos/[owner]/[repo]/issues/events.js" = "//pages/repos/[owner]/[repo]/issues/events.js", + .@"pages/repos/[owner]/[repo]/issues/events/[event_id].js" = "//pages/repos/[owner]/[repo]/issues/events/[event_id].js", + .@"pages/repos/[owner]/[repo]/keys.js" = "//pages/repos/[owner]/[repo]/keys.js", + .@"pages/repos/[owner]/[repo]/keys/[key_id].js" = "//pages/repos/[owner]/[repo]/keys/[key_id].js", + .@"pages/repos/[owner]/[repo]/labels.js" = "//pages/repos/[owner]/[repo]/labels.js", + .@"pages/repos/[owner]/[repo]/labels/[name].js" = "//pages/repos/[owner]/[repo]/labels/[name].js", + .@"pages/repos/[owner]/[repo]/languages.js" = "//pages/repos/[owner]/[repo]/languages.js", + .@"pages/repos/[owner]/[repo]/license.js" = "//pages/repos/[owner]/[repo]/license.js", + .@"pages/repos/[owner]/[repo]/milestones.js" = "//pages/repos/[owner]/[repo]/milestones.js", + .@"pages/repos/[owner]/[repo]/milestones/[milestone_number].js" = "//pages/repos/[owner]/[repo]/milestones/[milestone_number].js", + .@"pages/repos/[owner]/[repo]/milestones/[milestone_number]/labels.js" = "//pages/repos/[owner]/[repo]/milestones/[milestone_number]/labels.js", + .@"pages/repos/[owner]/[repo]/pages.js" = "//pages/repos/[owner]/[repo]/pages.js", + .@"pages/repos/[owner]/[repo]/pages/builds.js" = "//pages/repos/[owner]/[repo]/pages/builds.js", + .@"pages/repos/[owner]/[repo]/pages/builds/[build_id].js" = "//pages/repos/[owner]/[repo]/pages/builds/[build_id].js", + .@"pages/repos/[owner]/[repo]/pages/builds/latest.js" = "//pages/repos/[owner]/[repo]/pages/builds/latest.js", + .@"pages/repos/[owner]/[repo]/pages/health.js" = "//pages/repos/[owner]/[repo]/pages/health.js", + .@"pages/repos/[owner]/[repo]/projects.js" = "//pages/repos/[owner]/[repo]/projects.js", + .@"pages/repos/[owner]/[repo]/pulls.js" = "//pages/repos/[owner]/[repo]/pulls.js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number].js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number].js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number]/comments.js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number]/comments.js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number]/commits.js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number]/commits.js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number]/files.js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number]/files.js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number]/merge.js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number]/merge.js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number]/requested_reviewers.js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number]/requested_reviewers.js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number]/reviews.js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number]/reviews.js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number]/reviews/[review_id].js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number]/reviews/[review_id].js", + .@"pages/repos/[owner]/[repo]/pulls/[pull_number]/reviews/[review_id]/comments.js" = "//pages/repos/[owner]/[repo]/pulls/[pull_number]/reviews/[review_id]/comments.js", + .@"pages/repos/[owner]/[repo]/pulls/comments.js" = "//pages/repos/[owner]/[repo]/pulls/comments.js", + .@"pages/repos/[owner]/[repo]/pulls/comments/[comment_id].js" = "//pages/repos/[owner]/[repo]/pulls/comments/[comment_id].js", + .@"pages/repos/[owner]/[repo]/pulls/comments/[comment_id]/reactions.js" = "//pages/repos/[owner]/[repo]/pulls/comments/[comment_id]/reactions.js", + .@"pages/repos/[owner]/[repo]/readme.js" = "//pages/repos/[owner]/[repo]/readme.js", + .@"pages/repos/[owner]/[repo]/readme/[dir].js" = "//pages/repos/[owner]/[repo]/readme/[dir].js", + .@"pages/repos/[owner]/[repo]/releases.js" = "//pages/repos/[owner]/[repo]/releases.js", + .@"pages/repos/[owner]/[repo]/releases/[release_id].js" = "//pages/repos/[owner]/[repo]/releases/[release_id].js", + .@"pages/repos/[owner]/[repo]/releases/[release_id]/assets.js" = "//pages/repos/[owner]/[repo]/releases/[release_id]/assets.js", + .@"pages/repos/[owner]/[repo]/releases/assets/[asset_id].js" = "//pages/repos/[owner]/[repo]/releases/assets/[asset_id].js", + .@"pages/repos/[owner]/[repo]/releases/latest.js" = "//pages/repos/[owner]/[repo]/releases/latest.js", + .@"pages/repos/[owner]/[repo]/releases/tags/[tag].js" = "//pages/repos/[owner]/[repo]/releases/tags/[tag].js", + .@"pages/repos/[owner]/[repo]/secret-scanning/alerts.js" = "//pages/repos/[owner]/[repo]/secret-scanning/alerts.js", + .@"pages/repos/[owner]/[repo]/secret-scanning/alerts/[alert_number].js" = "//pages/repos/[owner]/[repo]/secret-scanning/alerts/[alert_number].js", + .@"pages/repos/[owner]/[repo]/stargazers.js" = "//pages/repos/[owner]/[repo]/stargazers.js", + .@"pages/repos/[owner]/[repo]/stats/code_frequency.js" = "//pages/repos/[owner]/[repo]/stats/code_frequency.js", + .@"pages/repos/[owner]/[repo]/stats/commit_activity.js" = "//pages/repos/[owner]/[repo]/stats/commit_activity.js", + .@"pages/repos/[owner]/[repo]/stats/contributors.js" = "//pages/repos/[owner]/[repo]/stats/contributors.js", + .@"pages/repos/[owner]/[repo]/stats/participation.js" = "//pages/repos/[owner]/[repo]/stats/participation.js", + .@"pages/repos/[owner]/[repo]/stats/punch_card.js" = "//pages/repos/[owner]/[repo]/stats/punch_card.js", + .@"pages/repos/[owner]/[repo]/subscribers.js" = "//pages/repos/[owner]/[repo]/subscribers.js", + .@"pages/repos/[owner]/[repo]/tags.js" = "//pages/repos/[owner]/[repo]/tags.js", + .@"pages/repos/[owner]/[repo]/tarball/[ref].js" = "//pages/repos/[owner]/[repo]/tarball/[ref].js", + .@"pages/repos/[owner]/[repo]/teams.js" = "//pages/repos/[owner]/[repo]/teams.js", + .@"pages/repos/[owner]/[repo]/topics.js" = "//pages/repos/[owner]/[repo]/topics.js", + .@"pages/repos/[owner]/[repo]/traffic/clones.js" = "//pages/repos/[owner]/[repo]/traffic/clones.js", + .@"pages/repos/[owner]/[repo]/traffic/popular/paths.js" = "//pages/repos/[owner]/[repo]/traffic/popular/paths.js", + .@"pages/repos/[owner]/[repo]/traffic/popular/referrers.js" = "//pages/repos/[owner]/[repo]/traffic/popular/referrers.js", + .@"pages/repos/[owner]/[repo]/traffic/views.js" = "//pages/repos/[owner]/[repo]/traffic/views.js", + .@"pages/repos/[owner]/[repo]/zipball/[ref].js" = "//pages/repos/[owner]/[repo]/zipball/[ref].js", + .@"pages/repositories.js" = "//pages/repositories.js", + .@"pages/repositories/[repository_id]/environments/[environment_name]/secrets.js" = "//pages/repositories/[repository_id]/environments/[environment_name]/secrets.js", + .@"pages/repositories/[repository_id]/environments/[environment_name]/secrets/[secret_name].js" = "//pages/repositories/[repository_id]/environments/[environment_name]/secrets/[secret_name].js", + .@"pages/repositories/[repository_id]/environments/[environment_name]/secrets/public-key.js" = "//pages/repositories/[repository_id]/environments/[environment_name]/secrets/public-key.js", + .@"pages/scim/v2/enterprises/[enterprise]/Groups.js" = "//pages/scim/v2/enterprises/[enterprise]/Groups.js", + .@"pages/scim/v2/enterprises/[enterprise]/Groups/[scim_group_id].js" = "//pages/scim/v2/enterprises/[enterprise]/Groups/[scim_group_id].js", + .@"pages/scim/v2/enterprises/[enterprise]/Users.js" = "//pages/scim/v2/enterprises/[enterprise]/Users.js", + .@"pages/scim/v2/enterprises/[enterprise]/Users/[scim_user_id].js" = "//pages/scim/v2/enterprises/[enterprise]/Users/[scim_user_id].js", + .@"pages/scim/v2/organizations/[org]/Users.js" = "//pages/scim/v2/organizations/[org]/Users.js", + .@"pages/scim/v2/organizations/[org]/Users/[scim_user_id].js" = "//pages/scim/v2/organizations/[org]/Users/[scim_user_id].js", + .@"pages/search/code.js" = "//pages/search/code.js", + .@"pages/search/commits.js" = "//pages/search/commits.js", + .@"pages/search/issues.js" = "//pages/search/issues.js", + .@"pages/search/labels.js" = "//pages/search/labels.js", + .@"pages/search/repositories.js" = "//pages/search/repositories.js", + .@"pages/search/topics.js" = "//pages/search/topics.js", + .@"pages/search/users.js" = "//pages/search/users.js", + .@"pages/teams/[team_id].js" = "//pages/teams/[team_id].js", + .@"pages/teams/[team_id]/discussions.js" = "//pages/teams/[team_id]/discussions.js", + .@"pages/teams/[team_id]/discussions/[discussion_number].js" = "//pages/teams/[team_id]/discussions/[discussion_number].js", + .@"pages/teams/[team_id]/discussions/[discussion_number]/comments.js" = "//pages/teams/[team_id]/discussions/[discussion_number]/comments.js", + .@"pages/teams/[team_id]/discussions/[discussion_number]/comments/[comment_number].js" = "//pages/teams/[team_id]/discussions/[discussion_number]/comments/[comment_number].js", + .@"pages/teams/[team_id]/discussions/[discussion_number]/comments/[comment_number]/reactions.js" = "//pages/teams/[team_id]/discussions/[discussion_number]/comments/[comment_number]/reactions.js", + .@"pages/teams/[team_id]/discussions/[discussion_number]/reactions.js" = "//pages/teams/[team_id]/discussions/[discussion_number]/reactions.js", + .@"pages/teams/[team_id]/invitations.js" = "//pages/teams/[team_id]/invitations.js", + .@"pages/teams/[team_id]/members.js" = "//pages/teams/[team_id]/members.js", + .@"pages/teams/[team_id]/members/[username].js" = "//pages/teams/[team_id]/members/[username].js", + .@"pages/teams/[team_id]/memberships/[username].js" = "//pages/teams/[team_id]/memberships/[username].js", + .@"pages/teams/[team_id]/projects.js" = "//pages/teams/[team_id]/projects.js", + .@"pages/teams/[team_id]/projects/[project_id].js" = "//pages/teams/[team_id]/projects/[project_id].js", + .@"pages/teams/[team_id]/repos.js" = "//pages/teams/[team_id]/repos.js", + .@"pages/teams/[team_id]/repos/[owner]/[repo].js" = "//pages/teams/[team_id]/repos/[owner]/[repo].js", + .@"pages/teams/[team_id]/teams.js" = "//pages/teams/[team_id]/teams.js", + .@"pages/users.js" = "//pages/users.js", + .@"pages/users/[username].js" = "//pages/users/[username].js", + .@"pages/users/[username]/events.js" = "//pages/users/[username]/events.js", + .@"pages/users/[username]/events/public.js" = "//pages/users/[username]/events/public.js", + .@"pages/users/[username]/followers.js" = "//pages/users/[username]/followers.js", + .@"pages/users/[username]/following.js" = "//pages/users/[username]/following.js", + .@"pages/users/[username]/following/[target_user].js" = "//pages/users/[username]/following/[target_user].js", + .@"pages/users/[username]/gpg_keys.js" = "//pages/users/[username]/gpg_keys.js", + .@"pages/users/[username]/keys.js" = "//pages/users/[username]/keys.js", + .@"pages/users/[username]/orgs.js" = "//pages/users/[username]/orgs.js", + .@"pages/users/[username]/received_events.js" = "//pages/users/[username]/received_events.js", + .@"pages/users/[username]/received_events/public.js" = "//pages/users/[username]/received_events/public.js", + .@"pages/users/[username]/repos.js" = "//pages/users/[username]/repos.js", + .@"pages/users/[username]/starred.js" = "//pages/users/[username]/starred.js", + .@"pages/users/[username]/subscriptions.js" = "//pages/users/[username]/subscriptions.js", + .@"pages/zen.js" = "//pages/zen.js", +}; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 09b399633..dc786c0b3 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -44,6 +44,11 @@ pub fn indexOfCharNeg(self: string, char: u8) i32 { return -1; } +pub fn indexOfSigned(self: string, str: string) i32 { + const i = std.mem.indexOf(u8, self, str) orelse return -1; + return @intCast(i32, i); +} + pub inline fn lastIndexOfChar(self: string, char: u8) ?usize { return std.mem.lastIndexOfScalar(u8, self, char); } @@ -203,6 +208,10 @@ test "StringOrTinyString Lowercase" { try std.testing.expectEqualStrings("hello!!!!!", str.slice()); } +pub fn hasPrefix(self: string, str: string) bool { + return str.len > 0 and startsWith(self, str); +} + pub fn startsWith(self: string, str: string) bool { if (str.len > self.len) { return false; @@ -347,7 +356,10 @@ inline fn eqlComptimeCheckLen(self: string, comptime alt: anytype, comptime chec }, 5, 7 => { const check = comptime std.mem.readIntNative(u32, alt[0..4]); - if (((comptime check_len) and self.len != alt.len) or std.mem.readIntNative(u32, self[0..4]) != check) { + if (((comptime check_len) and + self.len != alt.len) or + std.mem.readIntNative(u32, self[0..4]) != check) + { return false; } const remainder = self[4..]; @@ -566,7 +578,7 @@ pub fn encodeWTF8Rune(p: []u8, r: i32) u3 { } pub fn toUTF16Buf(in: string, out: []u16) usize { - var utf8Iterator = CodepointIterator{ .bytes = in, .i = 0 }; + var utf8Iterator = CodepointIterator.init(in); var c: u21 = 0; var i: usize = 0; @@ -595,7 +607,7 @@ pub fn toUTF16Buf(in: string, out: []u16) usize { } pub fn toUTF16Alloc(in: string, allocator: *std.mem.Allocator) !JavascriptString { - var utf8Iterator = CodepointIterator{ .bytes = in, .i = 0 }; + var utf8Iterator = CodepointIterator.init(in); var out = try std.ArrayList(u16).initCapacity(allocator, in.len); var c: u21 = 0; @@ -706,22 +718,43 @@ pub fn utf8ByteSequenceLength(first_byte: u8) u3 { pub fn NewCodePointIterator(comptime CodePointType: type, comptime zeroValue: comptime_int) type { return struct { const Iterator = @This(); - bytes: []const u8, - i: usize, + bytes: [*]const u8, + i: u32 = 0, + len: u32 = 0, width: u3 = 0, - c: CodePointType = 0, + c: CodePointType = zeroValue, + + pub fn initOffset(bytes: []const u8, offset: u32) Iterator { + return Iterator{ + .bytes = bytes.ptr, + .i = offset, + .len = @truncate(u32, bytes.len), + }; + } + + pub inline fn isEnd(this: Iterator) bool { + return this.c == zeroValue and @minimum(this.len, this.i) >= this.len; + } + + pub fn init(bytes: []const u8) Iterator { + return Iterator{ + .bytes = bytes.ptr, + .i = 0, + .len = @truncate(u32, bytes.len), + }; + } inline fn nextCodepointSlice(it: *Iterator) []const u8 { @setRuntimeSafety(false); const cp_len = utf8ByteSequenceLength(it.bytes[it.i]); - it.i += cp_len; + it.i = @minimum(it.i + cp_len, it.len); - return if (!(it.i > it.bytes.len)) it.bytes[it.i - cp_len .. it.i] else ""; + return if (!(it.i + 1 > it.len)) it.bytes[it.i - cp_len .. it.i] else ""; } pub fn needsUTF8Decoding(slice: string) bool { - var it = Iterator{ .bytes = slice, .i = 0 }; + var it = Iterator.init(slice); while (true) { const part = it.nextCodepointSlice(); @@ -788,6 +821,10 @@ pub fn NewCodePointIterator(comptime CodePointType: type, comptime zeroValue: co }; } + pub fn remaining(it: *Iterator) []const u8 { + return it.bytes[it.i..it.len]; + } + /// Look ahead at the next n codepoints without advancing the iterator. /// If fewer than n codepoints are available, then return the remainder of the string. pub fn peek(it: *Iterator, n: usize) []const u8 { @@ -797,7 +834,7 @@ pub fn NewCodePointIterator(comptime CodePointType: type, comptime zeroValue: co var end_ix = original_i; var found: usize = 0; while (found < n) : (found += 1) { - const next_codepoint = it.nextCodepointSlice() orelse return it.bytes[original_i..]; + const next_codepoint = it.nextCodepointSlice() orelse return it.bytes[original_i..it.len]; end_ix += next_codepoint.len; } diff --git a/src/string_types.zig b/src/string_types.zig index 83e47953e..31322959d 100644 --- a/src/string_types.zig +++ b/src/string_types.zig @@ -42,3 +42,46 @@ pub const PathString = packed struct { } } }; + +pub const HashedString = struct { + ptr: [*]const u8, + len: u32, + hash: u32, + + pub const empty = HashedString{ .ptr = @intToPtr([*]const u8, 0xDEADBEEF), .len = 0, .hash = 0 }; + + pub fn init(buf: string) HashedString { + return HashedString{ + .ptr = buf.ptr, + .len = @truncate(u32, buf.len), + .hash = @truncate(u32, std.hash.Wyhash.hash(0, buf)), + }; + } + + pub fn initNoHash(buf: string) HashedString { + return HashedString{ + .ptr = buf.ptr, + .len = @truncate(u32, buf.len), + .hash = 0, + }; + } + + pub fn eql(this: HashedString, other: anytype) bool { + return Eql(this, @TypeOf(other), other); + } + + pub fn Eql(this: HashedString, comptime Other: type, other: Other) bool { + switch (comptime Other) { + HashedString, *HashedString, *const HashedString => { + return ((@maximum(this.hash, other.hash) > 0 and this.hash == other.hash) or (this.ptr == other.ptr)) and this.len == other.len; + }, + else => { + return @as(usize, this.len) == other.len and @truncate(u32, std.hash.Wyhash.hash(0, other[0..other.len])) == this.hash; + }, + } + } + + pub fn str(this: HashedString) string { + return this.ptr[0..len]; + } +}; |