aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2023-01-21 23:11:31 -0800
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2023-01-21 23:11:31 -0800
commit99d2d6e607a31a1beab84e1169e2d0bc352a79cf (patch)
treeee03ff151357941463f4418eaf787f8e948a3fe1
parenta2cfdf0e1c0353428f0682969e3fcfa86cc2d6a7 (diff)
downloadbun-99d2d6e607a31a1beab84e1169e2d0bc352a79cf.tar.gz
bun-99d2d6e607a31a1beab84e1169e2d0bc352a79cf.tar.zst
bun-99d2d6e607a31a1beab84e1169e2d0bc352a79cf.zip
Support query string parameters in module resolution
-rw-r--r--src/bun.js/api/bun.zig21
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.cpp19
-rw-r--r--src/bun.js/bindings/bindings.zig10
-rw-r--r--src/bun.js/bindings/exports.zig28
-rw-r--r--src/bun.js/bindings/headers-cpp.h2
-rw-r--r--src/bun.js/bindings/headers.h4
-rw-r--r--src/bun.js/javascript.zig107
-rw-r--r--src/bun.js/module_loader.zig24
-rw-r--r--src/http_client_async.zig13
-rw-r--r--src/resolver/resolver.zig8
-rw-r--r--src/string_immutable.zig5
11 files changed, 181 insertions, 60 deletions
diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig
index bcaa5e09f..ad015e0ba 100644
--- a/src/bun.js/api/bun.zig
+++ b/src/bun.js/api/bun.zig
@@ -964,6 +964,7 @@ fn doResolveWithArgs(
comptime is_file_path: bool,
) ?JSC.JSValue {
var errorable: ErrorableZigString = undefined;
+ var query_string = ZigString.Empty;
if (comptime is_file_path) {
VirtualMachine.resolveFilePathForAPI(
@@ -971,6 +972,7 @@ fn doResolveWithArgs(
ctx.ptr(),
specifier,
from,
+ &query_string,
is_esm,
);
} else {
@@ -979,6 +981,7 @@ fn doResolveWithArgs(
ctx.ptr(),
specifier,
from,
+ &query_string,
is_esm,
);
}
@@ -988,7 +991,23 @@ fn doResolveWithArgs(
return null;
}
- return errorable.result.value.toValue(ctx.ptr());
+ if (query_string.len > 0) {
+ var stack = std.heap.stackFallback(1024, ctx.allocator());
+ const allocator = stack.get();
+ var arraylist = std.ArrayList(u8).initCapacity(allocator, 1024) catch unreachable;
+ defer arraylist.deinit();
+ arraylist.writer().print("{any}{any}", .{
+ errorable.result.value,
+ query_string,
+ }) catch {
+ JSC.JSError(allocator, "Failed to allocate memory", .{}, ctx, exception);
+ return null;
+ };
+
+ return ZigString.initUTF8(arraylist.items).toValueGC(ctx);
+ }
+
+ return errorable.result.value.toValue(ctx);
}
pub fn resolveSync(
diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp
index 74b8eab9e..b37e280f3 100644
--- a/src/bun.js/bindings/ZigGlobalObject.cpp
+++ b/src/bun.js/bindings/ZigGlobalObject.cpp
@@ -3585,9 +3585,14 @@ JSC::Identifier GlobalObject::moduleLoaderResolve(JSGlobalObject* globalObject,
res.success = false;
ZigString keyZ = toZigString(key, globalObject);
ZigString referrerZ = referrer && !referrer.isUndefinedOrNull() && referrer.isString() ? toZigString(referrer, globalObject) : ZigStringEmpty;
- Zig__GlobalObject__resolve(&res, globalObject, &keyZ, &referrerZ);
+ ZigString queryString = { 0, 0 };
+ Zig__GlobalObject__resolve(&res, globalObject, &keyZ, &referrerZ, &queryString);
if (res.success) {
+ if (queryString.len > 0) {
+ return JSC::Identifier::fromString(globalObject->vm(), makeString(Zig::toString(res.result.value), Zig::toString(queryString)));
+ }
+
return toIdentifier(res.result.value, globalObject);
} else {
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
@@ -3612,14 +3617,22 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* g
ErrorableZigString resolved;
auto moduleNameZ = toZigString(moduleNameValue, globalObject);
auto sourceOriginZ = sourceURL.isEmpty() ? ZigStringCwd : toZigString(sourceURL.fileSystemPath());
+ ZigString queryString = { 0, 0 };
resolved.success = false;
- Zig__GlobalObject__resolve(&resolved, globalObject, &moduleNameZ, &sourceOriginZ);
+ Zig__GlobalObject__resolve(&resolved, globalObject, &moduleNameZ, &sourceOriginZ, &queryString);
if (!resolved.success) {
throwException(scope, resolved.result.err, globalObject);
return promise->rejectWithCaughtException(globalObject, scope);
}
- auto result = JSC::importModule(globalObject, toIdentifier(resolved.result.value, globalObject),
+ JSC::Identifier resolvedIdentifier;
+ if (queryString.len == 0) {
+ resolvedIdentifier = toIdentifier(resolved.result.value, globalObject);
+ } else {
+ resolvedIdentifier = JSC::Identifier::fromString(vm, makeString(Zig::toString(resolved.result.value), Zig::toString(queryString)));
+ }
+
+ auto result = JSC::importModule(globalObject, resolvedIdentifier,
JSC::jsUndefined(), parameters, JSC::jsUndefined());
RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(globalObject, scope));
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 49e0fad23..d77a9dbb6 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -1354,9 +1354,15 @@ pub fn NewGlobalObject(comptime Type: type) type {
}
return ErrorableZigString.err(error.ImportFailed, ZigString.init(importNotImpl).toErrorInstance(global).asVoid());
}
- pub fn resolve(res: *ErrorableZigString, global: *JSGlobalObject, specifier: *ZigString, source: *ZigString) callconv(.C) void {
+ pub fn resolve(
+ res: *ErrorableZigString,
+ global: *JSGlobalObject,
+ specifier: *ZigString,
+ source: *ZigString,
+ query_string: *ZigString,
+ ) callconv(.C) void {
if (comptime @hasDecl(Type, "resolve")) {
- @call(.always_inline, Type.resolve, .{ res, global, specifier.*, source.*, true });
+ @call(.always_inline, Type.resolve, .{ res, global, specifier.*, source.*, query_string, true });
return;
}
res.* = ErrorableZigString.err(error.ResolveFailed, ZigString.init(resolveNotImpl).toErrorInstance(global).asVoid());
diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig
index d4956b1c4..54d855d23 100644
--- a/src/bun.js/bindings/exports.zig
+++ b/src/bun.js/bindings/exports.zig
@@ -54,43 +54,31 @@ pub const ZigGlobalObject = extern struct {
}
pub fn import(global: *JSGlobalObject, specifier: *ZigString, source: *ZigString) callconv(.C) ErrorableZigString {
- if (comptime is_bindgen) {
- unreachable;
- }
+ JSC.markBinding(@src());
return @call(.always_inline, Interface.import, .{ global, specifier, source });
}
- pub fn resolve(res: *ErrorableZigString, global: *JSGlobalObject, specifier: *ZigString, source: *ZigString) callconv(.C) void {
- if (comptime is_bindgen) {
- unreachable;
- }
- @call(.always_inline, Interface.resolve, .{ res, global, specifier, source });
+ pub fn resolve(res: *ErrorableZigString, global: *JSGlobalObject, specifier: *ZigString, source: *ZigString, query: *ZigString) callconv(.C) void {
+ JSC.markBinding(@src());
+ @call(.always_inline, Interface.resolve, .{ res, global, specifier, source, query });
}
pub fn fetch(ret: *ErrorableResolvedSource, global: *JSGlobalObject, specifier: *ZigString, source: *ZigString) callconv(.C) void {
- if (comptime is_bindgen) {
- unreachable;
- }
+ JSC.markBinding(@src());
@call(.always_inline, Interface.fetch, .{ ret, global, specifier, source });
}
pub fn promiseRejectionTracker(global: *JSGlobalObject, promise: *JSPromise, rejection: JSPromiseRejectionOperation) callconv(.C) JSValue {
- if (comptime is_bindgen) {
- unreachable;
- }
+ JSC.markBinding(@src());
return @call(.always_inline, Interface.promiseRejectionTracker, .{ global, promise, rejection });
}
pub fn reportUncaughtException(global: *JSGlobalObject, exception: *Exception) callconv(.C) JSValue {
- if (comptime is_bindgen) {
- unreachable;
- }
+ JSC.markBinding(@src());
return @call(.always_inline, Interface.reportUncaughtException, .{ global, exception });
}
pub fn onCrash() callconv(.C) void {
- if (comptime is_bindgen) {
- unreachable;
- }
+ JSC.markBinding(@src());
return @call(.always_inline, Interface.onCrash, .{});
}
diff --git a/src/bun.js/bindings/headers-cpp.h b/src/bun.js/bindings/headers-cpp.h
index a05007fff..969134e50 100644
--- a/src/bun.js/bindings/headers-cpp.h
+++ b/src/bun.js/bindings/headers-cpp.h
@@ -1,4 +1,4 @@
-//-- AUTOGENERATED FILE -- 1673376494
+//-- AUTOGENERATED FILE -- 1674359227
// clang-format off
#pragma once
diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h
index 71a8d1034..269698a98 100644
--- a/src/bun.js/bindings/headers.h
+++ b/src/bun.js/bindings/headers.h
@@ -1,5 +1,5 @@
// clang-format off
-//-- AUTOGENERATED FILE -- 1673376494
+//-- AUTOGENERATED FILE -- 1674359227
#pragma once
#include <stddef.h>
@@ -521,7 +521,7 @@ ZIG_DECL ErrorableZigString Zig__GlobalObject__import(JSC__JSGlobalObject* arg0,
ZIG_DECL void Zig__GlobalObject__onCrash();
ZIG_DECL JSC__JSValue Zig__GlobalObject__promiseRejectionTracker(JSC__JSGlobalObject* arg0, JSC__JSPromise* arg1, uint32_t JSPromiseRejectionOperation2);
ZIG_DECL JSC__JSValue Zig__GlobalObject__reportUncaughtException(JSC__JSGlobalObject* arg0, JSC__Exception* arg1);
-ZIG_DECL void Zig__GlobalObject__resolve(ErrorableZigString* arg0, JSC__JSGlobalObject* arg1, ZigString* arg2, ZigString* arg3);
+ZIG_DECL void Zig__GlobalObject__resolve(ErrorableZigString* arg0, JSC__JSGlobalObject* arg1, ZigString* arg2, ZigString* arg3, ZigString* arg4);
#endif
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig
index d9cfcaac3..49f9df802 100644
--- a/src/bun.js/javascript.zig
+++ b/src/bun.js/javascript.zig
@@ -880,8 +880,8 @@ pub const VirtualMachine = struct {
if (try ModuleLoader.fetchBuiltinModule(jsc_vm, _specifier, log, comptime flags.disableTranspiling())) |builtin| {
return builtin;
}
-
- var specifier = ModuleLoader.normalizeSpecifier(jsc_vm, _specifier);
+ var display_specifier = _specifier;
+ var specifier = ModuleLoader.normalizeSpecifier(jsc_vm, _specifier, &display_specifier);
var path = Fs.Path.init(specifier);
const loader = jsc_vm.bundler.options.loaders.get(path.name.ext) orelse brk: {
if (strings.eqlLong(specifier, jsc_vm.main, true)) {
@@ -894,6 +894,7 @@ pub const VirtualMachine = struct {
return try ModuleLoader.transpileSourceCode(
jsc_vm,
specifier,
+ display_specifier,
referrer,
path,
loader,
@@ -910,8 +911,22 @@ pub const VirtualMachine = struct {
pub const ResolveFunctionResult = struct {
result: ?Resolver.Result,
path: string,
+ query_string: []const u8 = "",
};
+ fn normalizeSpecifierForResolution(specifier_: []const u8, query_string: *[]const u8) []const u8 {
+ var specifier = specifier_;
+ if (strings.hasPrefixComptime(specifier, "file://")) specifier = specifier["file://".len..];
+
+ if (strings.indexOfChar(specifier, '?')) |i| {
+ specifier = specifier[0..i];
+ query_string.* = specifier[i..];
+ }
+
+ return specifier;
+ }
+
+ threadlocal var specifier_cache_resolver_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
fn _resolve(
ret: *ResolveFunctionResult,
_: *JSGlobalObject,
@@ -952,30 +967,65 @@ pub const VirtualMachine = struct {
}
const is_special_source = strings.eqlComptime(source, main_file_name) or js_ast.Macro.isMacroPath(source);
-
- const result = try switch (jsc_vm.bundler.resolver.resolveAndAutoInstall(
- if (!is_special_source)
- if (is_a_file_path)
- Fs.PathName.init(source).dirWithTrailingSlash()
- else
- source
+ var query_string: []const u8 = "";
+ const normalized_specifier = normalizeSpecifierForResolution(specifier, &query_string);
+ const source_to_use = if (!is_special_source)
+ if (is_a_file_path)
+ Fs.PathName.init(source).dirWithTrailingSlash()
else
- jsc_vm.bundler.fs.top_level_dir,
- // TODO: do we need to handle things like query string params?
- if (strings.hasPrefixComptime(specifier, "file://")) specifier["file://".len..] else specifier,
- if (is_esm) .stmt else .require,
- .read_only,
- )) {
- .success => |r| r,
- .failure => |e| e,
- .not_found => error.ModuleNotFound,
- .pending => unreachable,
+ source
+ else
+ jsc_vm.bundler.fs.top_level_dir;
+
+ const result: Resolver.Result = try brk: {
+ var retry_on_not_found = query_string.len > 0;
+ while (true) {
+ break :brk switch (jsc_vm.bundler.resolver.resolveAndAutoInstall(
+ source_to_use,
+ normalized_specifier,
+ if (is_esm) .stmt else .require,
+ .read_only,
+ )) {
+ .success => |r| r,
+ .failure => |e| e,
+ .pending => unreachable,
+ .not_found => if (!retry_on_not_found)
+ error.ModuleNotFound
+ else {
+ retry_on_not_found = false;
+
+ const buster_name = name: {
+ if (std.fs.path.isAbsolute(normalized_specifier)) {
+ if (std.fs.path.dirname(normalized_specifier)) |dir| {
+ break :name strings.withTrailingSlash(dir, normalized_specifier);
+ }
+ }
+
+ var parts = [_]string{
+ source_to_use,
+ normalized_specifier,
+ };
+
+ break :name bun.path.joinAbsStringBuf(
+ jsc_vm.bundler.fs.top_level_dir,
+ &specifier_cache_resolver_buf,
+ &parts,
+ .auto,
+ );
+ };
+
+ jsc_vm.bundler.resolver.bustDirCache(buster_name);
+ continue;
+ },
+ };
+ }
};
if (!jsc_vm.macro_mode) {
jsc_vm.has_any_macro_remappings = jsc_vm.has_any_macro_remappings or jsc_vm.bundler.options.macro_remap.count() > 0;
}
ret.result = result;
+ ret.query_string = query_string;
const result_path = result.pathConst() orelse return error.ModuleNotFound;
jsc_vm.resolved_count += 1;
if (comptime !realpath) {
@@ -1044,9 +1094,10 @@ pub const VirtualMachine = struct {
global: *JSGlobalObject,
specifier: ZigString,
source: ZigString,
+ query_string: *ZigString,
is_esm: bool,
) void {
- resolveMaybeNeedsTrailingSlash(res, global, specifier, source, is_esm, false, true);
+ resolveMaybeNeedsTrailingSlash(res, global, specifier, source, query_string, is_esm, false, true);
}
pub fn resolveFilePathForAPI(
@@ -1054,9 +1105,10 @@ pub const VirtualMachine = struct {
global: *JSGlobalObject,
specifier: ZigString,
source: ZigString,
+ query_string: *ZigString,
is_esm: bool,
) void {
- resolveMaybeNeedsTrailingSlash(res, global, specifier, source, is_esm, true, true);
+ resolveMaybeNeedsTrailingSlash(res, global, specifier, source, query_string, is_esm, true, true);
}
pub fn resolve(
@@ -1064,9 +1116,10 @@ pub const VirtualMachine = struct {
global: *JSGlobalObject,
specifier: ZigString,
source: ZigString,
+ query_string: *ZigString,
is_esm: bool,
) void {
- resolveMaybeNeedsTrailingSlash(res, global, specifier, source, is_esm, true, false);
+ resolveMaybeNeedsTrailingSlash(res, global, specifier, source, query_string, is_esm, true, false);
}
fn normalizeSource(source: []const u8) []const u8 {
@@ -1077,11 +1130,12 @@ pub const VirtualMachine = struct {
return source;
}
- pub fn resolveMaybeNeedsTrailingSlash(
+ fn resolveMaybeNeedsTrailingSlash(
res: *ErrorableZigString,
global: *JSGlobalObject,
specifier: ZigString,
source: ZigString,
+ query_string: ?*ZigString,
is_esm: bool,
comptime is_a_file_path: bool,
comptime realpath: bool,
@@ -1156,6 +1210,10 @@ pub const VirtualMachine = struct {
return;
};
+ if (query_string) |query| {
+ query.* = ZigString.init(result.query_string);
+ }
+
res.* = ErrorableZigString.ok(ZigString.init(result.path));
}
@@ -2726,8 +2784,7 @@ pub const HotReloader = struct {
entries_option = rfs.entries.get(file_path);
}
- rfs.bustEntriesCache(file_path);
- resolver.dir_cache.remove(file_path);
+ resolver.bustDirCache(file_path);
if (entries_option) |dir_ent| {
var last_file_hash: Watcher.HashType = std.math.maxInt(Watcher.HashType);
diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig
index b689dea83..f60c3fe29 100644
--- a/src/bun.js/module_loader.zig
+++ b/src/bun.js/module_loader.zig
@@ -887,6 +887,7 @@ pub const ModuleLoader = struct {
pub fn transpileSourceCode(
jsc_vm: *VirtualMachine,
specifier: string,
+ display_specifier: string,
referrer: string,
path: Fs.Path,
loader: options.Loader,
@@ -993,7 +994,7 @@ pub const ModuleLoader = struct {
.print_source => ZigString.init(parse_result.source.contents),
else => unreachable,
},
- .specifier = ZigString.init(specifier),
+ .specifier = ZigString.init(display_specifier),
.source_url = ZigString.init(path.text),
.hash = 0,
};
@@ -1105,7 +1106,7 @@ pub const ModuleLoader = struct {
}
if (jsc_vm.isWatcherEnabled()) {
- const resolved_source = jsc_vm.refCountedResolvedSource(printer.ctx.written, specifier, path.text, null);
+ const resolved_source = jsc_vm.refCountedResolvedSource(printer.ctx.written, display_specifier, path.text, null);
if (parse_result.input_fd) |fd_| {
if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) {
@@ -1127,7 +1128,7 @@ pub const ModuleLoader = struct {
return ResolvedSource{
.allocator = null,
.source_code = ZigString.init(try default_allocator.dupe(u8, printer.ctx.getWritten())),
- .specifier = ZigString.init(specifier),
+ .specifier = ZigString.init(display_specifier),
.source_url = ZigString.init(path.text),
// // TODO: change hash to a bitfield
// .hash = 1,
@@ -1328,7 +1329,7 @@ pub const ModuleLoader = struct {
}
}
}
- pub fn normalizeSpecifier(jsc_vm: *VirtualMachine, slice_: string) string {
+ pub fn normalizeSpecifier(jsc_vm: *VirtualMachine, slice_: string, string_to_use_for_source: *[]const u8) string {
var slice = slice_;
if (slice.len == 0) return slice;
var was_http = false;
@@ -1360,6 +1361,12 @@ pub const ModuleLoader = struct {
}
}
+ string_to_use_for_source.* = slice;
+
+ if (strings.indexOfChar(slice, '?')) |i| {
+ slice = slice[0..i];
+ }
+
return slice;
}
@@ -1405,7 +1412,12 @@ pub const ModuleLoader = struct {
var referrer_slice = referrer.toSlice(jsc_vm.allocator);
defer _specifier.deinit();
defer referrer_slice.deinit();
- var specifier = normalizeSpecifier(jsc_vm, _specifier.slice());
+ var display_specifier: []const u8 = "";
+ var specifier = normalizeSpecifier(
+ jsc_vm,
+ _specifier.slice(),
+ &display_specifier,
+ );
const path = Fs.Path.init(specifier);
const loader = jsc_vm.bundler.options.loaders.get(path.name.ext) orelse options.Loader.js;
var promise: ?*JSC.JSInternalPromise = null;
@@ -1413,6 +1425,7 @@ pub const ModuleLoader = struct {
ModuleLoader.transpileSourceCode(
jsc_vm,
specifier,
+ display_specifier,
referrer_slice.slice(),
path,
loader,
@@ -1977,6 +1990,7 @@ pub const ModuleLoader = struct {
ModuleLoader.transpileSourceCode(
jsc_vm,
specifier,
+ specifier,
referrer_slice.slice(),
path,
options.Loader.fromString(@tagName(loader)).?,
diff --git a/src/http_client_async.zig b/src/http_client_async.zig
index 41b12fce3..853ab8f3d 100644
--- a/src/http_client_async.zig
+++ b/src/http_client_async.zig
@@ -1178,7 +1178,18 @@ pub const AsyncHTTP = struct {
};
const AtomicState = std.atomic.Atomic(State);
- pub fn init(allocator: std.mem.Allocator, method: Method, url: URL, headers: Headers.Entries, headers_buf: string, response_buffer: *MutableString, request_body: []const u8, timeout: usize, callback: HTTPClientResult.Callback, http_proxy: ?URL) AsyncHTTP {
+ pub fn init(
+ allocator: std.mem.Allocator,
+ method: Method,
+ url: URL,
+ headers: Headers.Entries,
+ headers_buf: string,
+ response_buffer: *MutableString,
+ request_body: []const u8,
+ timeout: usize,
+ callback: HTTPClientResult.Callback,
+ http_proxy: ?URL,
+ ) AsyncHTTP {
var this = AsyncHTTP{ .allocator = allocator, .url = url, .method = method, .request_headers = headers, .request_header_buf = headers_buf, .request_body = request_body, .response_buffer = response_buffer, .completion_callback = callback, .http_proxy = http_proxy };
this.client = HTTPClient.init(allocator, method, url, headers, headers_buf);
this.client.timeout = timeout;
diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig
index f2a81a35a..5ba5eb74f 100644
--- a/src/resolver/resolver.zig
+++ b/src/resolver/resolver.zig
@@ -1373,6 +1373,14 @@ pub const Resolver = struct {
}
}
+ const dev = Output.scoped(.Resolver, false);
+
+ pub fn bustDirCache(r: *ThisResolver, path: string) void {
+ dev("Bust {s}", .{path});
+ r.fs.fs.bustEntriesCache(path);
+ r.dir_cache.remove(path);
+ }
+
threadlocal var esm_subpath_buf: [512]u8 = undefined;
threadlocal var esm_absolute_package_path: [bun.MAX_PATH_BYTES]u8 = undefined;
threadlocal var esm_absolute_package_path_joined: [bun.MAX_PATH_BYTES]u8 = undefined;
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index e4a92fb26..27f9ac5d7 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -553,6 +553,11 @@ pub fn withoutTrailingSlash(this: string) []const u8 {
return href;
}
+pub fn withTrailingSlash(dir: string, in: string) []const u8 {
+ std.debug.assert(bun.isSliceInBuffer(dir, in));
+ return in[0..@min(strings.withoutTrailingSlash(in[0..@min(dir.len + 1, in.len)]).len + 1, in.len)];
+}
+
pub fn withoutLeadingSlash(this: string) []const u8 {
return std.mem.trimLeft(u8, this, "/");
}