Skip to content

Commit 6a879d4

Browse files
refactor!: inline url utils (#440)
1 parent 3cf498b commit 6a879d4

File tree

2 files changed

+120
-1
lines changed

2 files changed

+120
-1
lines changed

src/fetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Readable } from "node:stream";
22
import destr from "destr";
3-
import { withBase, withQuery } from "ufo";
3+
import { withBase, withQuery } from "./utils.url.ts";
44
import { createFetchError } from "./error.ts";
55
import {
66
isPayloadMethod,

src/utils.url.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* eslint-disable unicorn/prefer-at */
2+
export type QueryValue =
3+
| string
4+
| number
5+
| boolean
6+
| QueryValue[]
7+
| Record<string, any>
8+
| null
9+
| undefined;
10+
export type QueryObject = Record<string, QueryValue | QueryValue[]>;
11+
12+
/**
13+
* Joins the given base URL and path, ensuring that there is only one slash between them.
14+
*/
15+
export function joinURL(base?: string, path?: string): string {
16+
if (!base || base === "/") {
17+
return path || "/";
18+
}
19+
20+
if (!path || path === "/") {
21+
return base || "/";
22+
}
23+
24+
const baseHasTrailing = base[base.length - 1] === "/";
25+
const pathHasLeading = path[0] === "/";
26+
if (baseHasTrailing && pathHasLeading) {
27+
return base + path.slice(1);
28+
}
29+
30+
if (!baseHasTrailing && !pathHasLeading) {
31+
return `${base}/${path}`;
32+
}
33+
34+
return base + path;
35+
}
36+
37+
/**
38+
* Adds the base path to the input path, if it is not already present.
39+
*/
40+
export function withBase(input = "", base = ""): string {
41+
if (!base || base === "/") {
42+
return input;
43+
}
44+
45+
const _base = withoutTrailingSlash(base);
46+
if (input.startsWith(_base)) {
47+
return input;
48+
}
49+
50+
return joinURL(_base, input);
51+
}
52+
53+
function withoutTrailingSlash(path?: string): string {
54+
if (!path || path === "/") {
55+
return "/";
56+
}
57+
58+
return path[path.length - 1] === "/" ? path.slice(0, -1) : path;
59+
}
60+
61+
/**
62+
* Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
63+
*/
64+
export function withQuery(input: string, query?: QueryObject): string {
65+
if (!query || Object.keys(query).length === 0) {
66+
return input;
67+
}
68+
69+
const searchIndex = input.indexOf("?");
70+
71+
if (searchIndex === -1) {
72+
const normalizedQuery = Object.entries(query)
73+
.filter(([, value]) => value !== undefined)
74+
.flatMap(([key, value]) => {
75+
if (Array.isArray(value)) {
76+
return value.map((item) => [key, normalizeQueryValue(item)]);
77+
}
78+
79+
return [[key, normalizeQueryValue(value)]];
80+
});
81+
const searchParams = new URLSearchParams(normalizedQuery);
82+
const queryString = searchParams.toString();
83+
return queryString ? `${input}?${queryString}` : input;
84+
}
85+
86+
const searchParams = new URLSearchParams(input.slice(searchIndex + 1));
87+
const base = input.slice(0, searchIndex);
88+
89+
for (const [key, value] of Object.entries(query)) {
90+
if (value === undefined) {
91+
searchParams.delete(key);
92+
} else if (Array.isArray(value)) {
93+
for (const item of value) {
94+
searchParams.append(key, normalizeQueryValue(item));
95+
}
96+
} else {
97+
searchParams.set(key, normalizeQueryValue(value));
98+
}
99+
}
100+
101+
const queryString = searchParams.toString();
102+
return queryString ? `${base}?${queryString}` : base;
103+
}
104+
105+
function normalizeQueryValue(value: QueryValue): string {
106+
if (value === null) {
107+
return "";
108+
}
109+
110+
if (typeof value === "number" || typeof value === "boolean") {
111+
return String(value);
112+
}
113+
114+
if (typeof value === "object") {
115+
return JSON.stringify(value);
116+
}
117+
118+
return String(value);
119+
}

0 commit comments

Comments
 (0)