UNPKG

build-url-ts

Version:

A small library that builds a URL given its components

1 lines 19.1 kB
{"version":3,"file":"index.cjs","sources":["../src/index.ts"],"sourcesContent":["/**\n * `build-url-ts` — a small, fast, zero-dependency library for composing URLs\n * from their parts (path, query string, and hash fragment).\n *\n * It runs anywhere JavaScript does (Node.js, Bun, Deno, edge runtimes, and\n * browsers) and ships both ESM and CommonJS builds with full type definitions.\n *\n * @packageDocumentation\n * @example\n * ```ts\n * import { buildUrl } from 'build-url-ts';\n *\n * buildUrl('https://api.example.com', {\n * path: 'users/123',\n * queryParams: { tab: 'profile', limit: 10 },\n * hash: 'summary',\n * });\n * // → https://api.example.com/users/123?tab=profile&limit=10#summary\n * ```\n */\n\n/**\n * A value accepted for a single query parameter.\n *\n * - `string` / `number` / `boolean` are stringified as-is.\n * - `null` becomes an empty value (`key=`).\n * - `undefined` is omitted entirely.\n * - An array is rendered as a comma-separated list by default, or as repeated /\n * indexed keys via {@link IBuildUrlOptions.disableCSV}.\n * - A `Date` is serialized with `Date.prototype.toString()`.\n * - Any other `object` is serialized with `JSON.stringify()`.\n */\nexport type QueryParamValue =\n | null\n | undefined\n | string\n | number\n | boolean\n | (string | number | boolean | null | undefined)[]\n | Date\n | object;\n\n/** A map of query parameter names to {@link QueryParamValue}s. */\nexport type IQueryParams = Record<string, QueryParamValue>;\n\n/**\n * How array query parameters are serialized when CSV joining is disabled.\n *\n * - `'array'` → `key[]=a&key[]=b`\n * - `'order_asc'` → `key[0]=a&key[1]=b`\n * - `'order_desc'` → `key[1]=a&key[0]=b`\n *\n * The boolean `true` (see {@link IBuildUrlOptions.disableCSV}) produces repeated\n * keys without brackets: `key=a&key=b`.\n */\nexport type IDisableCsvType = 'array' | 'order_asc' | 'order_desc';\n\n/** Options describing the parts of the URL to build. */\nexport interface IBuildUrlOptions {\n /**\n * A single path segment appended to the URL. Leading, trailing, and duplicate\n * slashes are normalized.\n *\n * @example 'about/me' // → /about/me\n */\n path?: string | number;\n /**\n * Multiple path segments appended in order. Use this instead of (or in\n * addition to) {@link IBuildUrlOptions.path}; when both are given, `path` is\n * applied first, then each entry of `paths`.\n *\n * @example ['about', '/my/', '/cat'] // → /about/my/cat\n */\n paths?: (string | number)[];\n /** Lowercase the generated path, query string, and hash. Defaults to `false`. */\n lowerCase?: boolean;\n /** Query parameters to append, merged on top of any already present on the URL. */\n queryParams?: IQueryParams;\n /**\n * Control how array query parameters are rendered. `false`/omitted joins them\n * into a comma-separated list; `true` repeats the key; a {@link IDisableCsvType}\n * selects a bracketed format.\n */\n disableCSV?: boolean | IDisableCsvType;\n /** Hash/fragment identifier to append (without the leading `#`). */\n hash?: string | number;\n}\n\n/**\n * Encodes a string with `encodeURIComponent`, additionally escaping the\n * single quote (`'`) and backtick (`` ` ``) which `encodeURIComponent` leaves\n * untouched but which can be ambiguous inside a URL.\n */\nfunction customEncodeURIComponent(str: string): string {\n return encodeURIComponent(str).replace(/'/g, '%27').replace(/`/g, '%60');\n}\n\n/**\n * Builds a query string (including the leading `?`) from a parameters object.\n *\n * @param queryParams - The parameters to serialize.\n * @param lowerCase - Lowercase keys and values. Defaults to `false`.\n * @param disableCSV - How to render array values. See {@link IDisableCsvType}.\n * @param useCustomEncoding - Use {@link customEncodeURIComponent} (escapes `'`\n * and `` ` ``) instead of plain `encodeURIComponent`. Defaults to `true`.\n * {@link buildUrl} passes `false` because it encodes values itself.\n * @returns The query string (e.g. `?foo=bar&bar=baz`), or `''` when empty.\n *\n * @example\n * ```ts\n * buildQueryString({ foo: 'bar', ids: [1, 2, 3] });\n * // → ?foo=bar&ids=1%2C2%2C3\n * ```\n */\nexport function buildQueryString(\n queryParams: IQueryParams,\n lowerCase?: boolean,\n disableCSV?: boolean | IDisableCsvType,\n useCustomEncoding: boolean = true\n): string {\n const encode = (input: string): string =>\n useCustomEncoding ? customEncodeURIComponent(input) : encodeURIComponent(input);\n const queryParts: string[] = [];\n\n for (const [key, value] of Object.entries(queryParams)) {\n // `undefined` means \"omit this parameter entirely\".\n if (value === undefined) continue;\n\n const encodedKey = encode(lowerCase ? key.toLowerCase() : key);\n\n if (!Array.isArray(value)) {\n queryParts.push(`${encodedKey}=${encode(formatValue(value, lowerCase))}`);\n continue;\n }\n\n // Drop `undefined` array items, then skip the parameter if nothing remains.\n const items = value.filter((item) => item !== undefined);\n if (items.length === 0) continue;\n\n if (!disableCSV) {\n // Default: join into a single comma-separated value.\n const csvValue = items.map((item) => formatValue(item, lowerCase)).join(',');\n queryParts.push(`${encodedKey}=${encode(csvValue)}`);\n continue;\n }\n\n // Otherwise render one entry per item, in the requested key format.\n let index = disableCSV === 'order_desc' ? items.length - 1 : 0;\n for (const item of items) {\n const encodedValue = encode(formatValue(item, lowerCase));\n switch (disableCSV) {\n case 'array':\n queryParts.push(`${encodedKey}[]=${encodedValue}`);\n break;\n case 'order_asc':\n queryParts.push(`${encodedKey}[${index++}]=${encodedValue}`);\n break;\n case 'order_desc':\n queryParts.push(`${encodedKey}[${index--}]=${encodedValue}`);\n break;\n default:\n queryParts.push(`${encodedKey}=${encodedValue}`);\n break;\n }\n }\n }\n\n return queryParts.length > 0 ? `?${queryParts.join('&')}` : '';\n}\n\n/**\n * Converts a single query value into its string form, before URL encoding.\n * `null`/`undefined`/empty become `''`; `Date` and plain objects are\n * serialized; everything else is stringified and trimmed.\n */\nfunction formatValue(value: QueryParamValue, lowerCase?: boolean): string {\n if (value === null || value === undefined) return '';\n if (typeof value === 'boolean') return value.toString();\n // Guard `0` before the falsy check below so it is not treated as empty.\n if (value === 0) return '0';\n if (typeof value === 'number' && Number.isNaN(value)) return 'NaN';\n if (!value) return '';\n\n const stringValue =\n value instanceof Date\n ? value.toString()\n : typeof value === 'object' && !Array.isArray(value)\n ? JSON.stringify(value)\n : String(value).trim();\n\n return lowerCase ? stringValue.toLowerCase() : stringValue;\n}\n\n/**\n * Appends a single path segment to a URL, normalizing slashes so there are no\n * empty or doubled segments while any meaningful trailing slash is preserved.\n *\n * @param path - The segment to append.\n * @param builtUrl - The URL built so far.\n * @param lowerCase - Lowercase the segment. Defaults to `false`.\n * @returns The URL with the segment appended.\n *\n * @example\n * ```ts\n * appendPath('users/123', 'https://api.example.com');\n * // → https://api.example.com/users/123\n * ```\n */\nexport function appendPath(path: string | number, builtUrl: string, lowerCase?: boolean): string {\n const url = builtUrl ?? '';\n const trimmedPath = String(path).trim();\n const pathString = lowerCase ? trimmedPath.toLowerCase() : trimmedPath;\n\n if (!pathString) return url;\n\n // A lone '/' only ensures a single trailing slash.\n if (pathString === '/') {\n return url.endsWith('/') ? url : `${url}/`;\n }\n\n // Collapse repeated slashes within the segment while keeping a trailing one.\n const hasTrailingSlash = pathString.endsWith('/');\n const cleanedPath = pathString\n .split('/')\n .filter((segment) => segment.length > 0)\n .join('/');\n const finalPath = hasTrailingSlash && cleanedPath ? `${cleanedPath}/` : cleanedPath;\n\n // Strip trailing slashes from the base before joining with a single '/'.\n const baseUrl = url.replace(/\\/+$/, '');\n return finalPath ? `${baseUrl}/${finalPath}` : baseUrl;\n}\n\n/**\n * Builds a hash fragment (including the leading `#`) from a value.\n *\n * @param hash - The fragment text (without `#`).\n * @param lowerCase - Lowercase the fragment. Defaults to `false`.\n * @returns The hash fragment (e.g. `#section`), or `''` when empty.\n *\n * @example\n * ```ts\n * buildHash('Section-1', true); // → #section-1\n * ```\n */\nexport function buildHash(hash: string | number, lowerCase?: boolean): string {\n const trimmedHash = String(hash).trim();\n if (!trimmedHash) return '';\n\n const hashString = `#${trimmedHash}`;\n return lowerCase ? hashString.toLowerCase() : hashString;\n}\n\n/**\n * Splits an existing URL into its base, query parameters, and hash so new\n * options can be merged on top of what is already present.\n */\nfunction parseUrl(url: string): { baseUrl: string; queryParams: IQueryParams; hash: string } {\n const queryParams: IQueryParams = {};\n\n // Split off the hash first; everything after the first '#' is the fragment.\n const hashIndex = url.indexOf('#');\n const hash = hashIndex === -1 ? '' : url.substring(hashIndex + 1);\n const withoutHash = hashIndex === -1 ? url : url.substring(0, hashIndex);\n\n // Then split off the query string at the first '?'.\n const queryIndex = withoutHash.indexOf('?');\n if (queryIndex === -1) {\n return { baseUrl: withoutHash, queryParams, hash };\n }\n\n const baseUrl = withoutHash.substring(0, queryIndex);\n const queryString = withoutHash.substring(queryIndex + 1);\n\n for (const pair of queryString.split('&')) {\n if (!pair) continue;\n // Split on the first '=' only so values may themselves contain '='.\n const eqIndex = pair.indexOf('=');\n const rawKey = eqIndex === -1 ? pair : pair.substring(0, eqIndex);\n const rawValue = eqIndex === -1 ? '' : pair.substring(eqIndex + 1);\n if (rawKey) {\n queryParams[decodeURIComponent(rawKey)] = rawValue ? decodeURIComponent(rawValue) : '';\n }\n }\n\n return { baseUrl, queryParams, hash };\n}\n\n/**\n * Builds a complete URL from a base URL and/or a set of options.\n *\n * Query parameters and hash already present on the base URL are preserved and\n * merged with the supplied options (options take precedence on conflicts).\n *\n * @param url - The base URL, or — when called with a single argument — the\n * options object itself. `null`/`undefined` builds a relative URL.\n * @param options - The parts to add. See {@link IBuildUrlOptions}.\n * @returns The constructed URL string.\n *\n * @example\n * ```ts\n * // Base URL plus options\n * buildUrl('https://example.com', { path: 'about', hash: 'team' });\n * // → https://example.com/about#team\n *\n * // Options only (relative URL)\n * buildUrl({ path: 'api/v2', queryParams: { format: 'json' } });\n * // → /api/v2?format=json\n *\n * // Multiple path segments\n * buildUrl('https://example.com', { paths: ['about', '/my/', '/cat'] });\n * // → https://example.com/about/my/cat\n * ```\n */\nfunction buildUrl(url?: string | null | IBuildUrlOptions, options?: IBuildUrlOptions): string {\n // Resolve the overloaded first argument into a base URL + options, capturing\n // any query/hash already present on a string URL so they can be merged.\n const hasStringUrl = typeof url === 'string';\n const parsed = hasStringUrl ? parseUrl(url) : null;\n const buildOptions = url !== null && typeof url === 'object' ? url : options;\n\n let result = parsed?.baseUrl ?? '';\n\n // 1. Path — the single `path` (applied first, for backward compatibility),\n // then each segment of `paths`, in order.\n const segments: (string | number)[] = [];\n if (buildOptions?.path) segments.push(buildOptions.path);\n if (buildOptions?.paths) segments.push(...buildOptions.paths);\n for (const segment of segments) {\n result = appendPath(segment, result, buildOptions?.lowerCase);\n }\n\n // 2. Query string — merge existing params with the supplied ones.\n const allQueryParams = { ...parsed?.queryParams, ...buildOptions?.queryParams };\n if (Object.keys(allQueryParams).length > 0) {\n // Values are already decoded here, so skip the extra custom encoding pass.\n result += buildQueryString(allQueryParams, buildOptions?.lowerCase, buildOptions?.disableCSV, false);\n }\n\n // 3. Hash — a supplied hash wins, otherwise keep the URL's existing one.\n const finalHash = buildOptions?.hash !== undefined ? buildOptions.hash : (parsed?.hash ?? '');\n if (finalHash) {\n result += buildHash(finalHash, buildOptions?.lowerCase);\n }\n\n return result;\n}\n\nexport { buildUrl };\nexport default buildUrl;\n"],"names":[],"mappings":"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"}