aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-10-18 23:55:17 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-10-18 23:55:17 -0700
commit3dc53c3d1327f9e86467d6509cb94faacfd26580 (patch)
tree7a502ffaa8d093ee93ce54a95add1b3f35d05f01
parent4f2c1cfe853dadced1f6508887d752dc671ae371 (diff)
downloadbun-3dc53c3d1327f9e86467d6509cb94faacfd26580.tar.gz
bun-3dc53c3d1327f9e86467d6509cb94faacfd26580.tar.zst
bun-3dc53c3d1327f9e86467d6509cb94faacfd26580.zip
Starting to rewrite the router to fix some bugs and support catch-all + optional routes
Diffstat (limited to '')
-rw-r--r--src/env_loader.zig2
-rw-r--r--src/js_lexer.zig2
-rw-r--r--src/logger.zig9
-rw-r--r--src/resolver/resolve_path.zig6
-rw-r--r--src/resolver/resolver.zig4
-rw-r--r--src/router.zig1223
-rw-r--r--src/string_immutable.zig57
-rw-r--r--src/string_types.zig43
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];
+ }
+};