diff --git a/src/utils.ts b/src/utils.ts index a3f99779..9e3345e0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,6 +14,7 @@ const PROTOCOL_RELATIVE_REGEX = /^([/\\]\s*){2,}[^/\\]/; const PROTOCOL_SCRIPT_RE = /^[\s\0]*(blob|data|javascript|vbscript):$/i; const TRAILING_SLASH_RE = /\/$|\/\?|\/#/; const JOIN_LEADING_SLASH_RE = /^\.?\//; +const JOIN_LAST_SEGMENT_RE = /(?:^|\/)[^/]*\/?$/; /** * Check if a path starts with `./` or `../`. @@ -323,9 +324,17 @@ export function joinURL(base: string, ...input: string[]): string { for (const segment of input.filter((url) => isNonEmptyURL(url))) { if (url) { - // TODO: Handle .. when joining - const _segment = segment.replace(JOIN_LEADING_SLASH_RE, ""); - url = withTrailingSlash(url) + _segment; + const hasAbsoluteBase = url.startsWith("/"); + let _segment = segment; + _segment = _segment.replace(JOIN_LEADING_SLASH_RE, ""); + while (url.length > 0 && _segment.startsWith("../")) { + url = url.replace(JOIN_LAST_SEGMENT_RE, ""); + _segment = _segment.slice(3).replace(JOIN_LEADING_SLASH_RE, ""); + } + url = + !url && (!hasAbsoluteBase || _segment.startsWith("../")) + ? _segment + : withTrailingSlash(url) + _segment; } else { url = segment; } diff --git a/test/join.test.ts b/test/join.test.ts index e7c400bb..0135200a 100644 --- a/test/join.test.ts +++ b/test/join.test.ts @@ -7,9 +7,21 @@ describe("joinURL", () => { { input: ["/"], out: "/" }, // eslint-disable-next-line unicorn/no-null { input: [null, "./"], out: "./" }, + { input: ["./", "a"], out: "./a" }, + { input: ["./a", "./b"], out: "./a/b" }, { input: ["/a"], out: "/a" }, { input: ["a", "b"], out: "a/b" }, { input: ["/", "/b"], out: "/b" }, + { input: ["/a", "../b"], out: "/b" }, + { input: ["../a", "../b"], out: "../b" }, + { input: ["../a", "./../b"], out: "../b" }, + { input: ["../a", "./../../b"], out: "b" }, + { input: ["../a", "../../../b"], out: "../b" }, + { input: ["../a", "../../../../b"], out: "../../b" }, + { input: ["../a/", "../b"], out: "../b" }, + { input: ["/a/b/c", "../../d"], out: "/a/d" }, + { input: ["/c", "../../d"], out: "../d" }, + { input: ["/c", ".././../d"], out: "../d" }, { input: ["a", "b/", "c"], out: "a/b/c" }, { input: ["a", "b/", "/c"], out: "a/b/c" }, { input: ["/", "./"], out: "/" },