nuqs
Version:
Type-safe search params state manager for React - Like useState, but stored in the URL query string
477 lines (466 loc) • 13 kB
JavaScript
import * as React from 'react';
// src/cache.ts
// src/errors.ts
var errors = {
303: "Multiple adapter contexts detected. This might happen in monorepos.",
404: "nuqs requires an adapter to work with your framework.",
409: "Multiple versions of the library are loaded. This may lead to unexpected behavior. Currently using `%s`, but `%s` (via the %s adapter) was about to load on top.",
414: "Max safe URL length exceeded. Some browsers may not be able to accept this URL. Consider limiting the amount of state stored in the URL.",
429: "URL update rate-limited by the browser. Consider increasing `throttleMs` for key(s) `%s`. %O",
500: "Empty search params cache. Search params can't be accessed in Layouts.",
501: "Search params cache already populated. Have you called `parse` twice?"
};
function error(code) {
return `[nuqs] ${errors[code]}
See https://err.47ng.com/NUQS-${code}`;
}
// src/loader.ts
function createLoader(parsers, { urlKeys = {} } = {}) {
function loadSearchParams(input) {
if (input instanceof Promise) {
return input.then((i) => loadSearchParams(i));
}
const searchParams = extractSearchParams(input);
const result = {};
for (const [key, parser] of Object.entries(parsers)) {
const urlKey = urlKeys[key] ?? key;
const value = searchParams.get(urlKey);
result[key] = parser.parseServerSide(value ?? void 0);
}
return result;
}
return loadSearchParams;
}
function extractSearchParams(input) {
try {
if (input instanceof Request) {
if (input.url) {
return new URL(input.url).searchParams;
} else {
return new URLSearchParams();
}
}
if (input instanceof URL) {
return input.searchParams;
}
if (input instanceof URLSearchParams) {
return input;
}
if (typeof input === "object") {
const entries = Object.entries(input);
const searchParams = new URLSearchParams();
for (const [key, value] of entries) {
if (Array.isArray(value)) {
for (const v of value) {
searchParams.append(key, v);
}
} else if (value !== void 0) {
searchParams.set(key, value);
}
}
return searchParams;
}
if (typeof input === "string") {
if ("canParse" in URL && URL.canParse(input)) {
return new URL(input).searchParams;
}
return new URLSearchParams(input);
}
} catch (e) {
return new URLSearchParams();
}
return new URLSearchParams();
}
// src/cache.ts
var $input = Symbol("Input");
function createSearchParamsCache(parsers, { urlKeys = {} } = {}) {
const load = createLoader(parsers, { urlKeys });
const getCache = React.cache(() => ({
searchParams: {}
}));
function parseSync(searchParams) {
const c = getCache();
if (Object.isFrozen(c.searchParams)) {
if (c[$input] && compareSearchParams(searchParams, c[$input])) {
return all();
}
throw new Error(error(501));
}
c.searchParams = load(searchParams);
c[$input] = searchParams;
return Object.freeze(c.searchParams);
}
function parse(searchParams) {
if (searchParams instanceof Promise) {
return searchParams.then(parseSync);
}
return parseSync(searchParams);
}
function all() {
const { searchParams } = getCache();
if (Object.keys(searchParams).length === 0) {
throw new Error(error(500));
}
return searchParams;
}
function get(key) {
const { searchParams } = getCache();
const entry = searchParams[key];
if (typeof entry === "undefined") {
throw new Error(
error(500) + `
in get(${String(key)})`
);
}
return entry;
}
return { parse, get, all };
}
function compareSearchParams(a, b) {
if (a === b) {
return true;
}
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (const key in a) {
if (a[key] !== b[key]) {
return false;
}
}
return true;
}
// src/debug.ts
var debugEnabled = isDebugEnabled();
function warn(message, ...args) {
if (!debugEnabled) {
return;
}
console.warn(message, ...args);
}
function isDebugEnabled() {
try {
if (typeof localStorage === "undefined") {
return false;
}
const test = "nuqs-localStorage-test";
localStorage.setItem(test, test);
const isStorageAvailable = localStorage.getItem(test) === test;
localStorage.removeItem(test);
if (!isStorageAvailable) {
return false;
}
} catch (error2) {
console.error(
"[nuqs]: debug mode is disabled (localStorage unavailable).",
error2
);
return false;
}
const debug = localStorage.getItem("debug") ?? "";
return debug.includes("nuqs");
}
// src/utils.ts
function safeParse(parser, value, key) {
try {
return parser(value);
} catch (error2) {
warn(
"[nuqs] Error while parsing value `%s`: %O" + (key ? " (for key `%s`)" : ""),
value,
error2,
key
);
return null;
}
}
// src/parsers.ts
function createParser(parser) {
function parseServerSideNullable(value) {
if (typeof value === "undefined") {
return null;
}
let str = "";
if (Array.isArray(value)) {
if (value[0] === void 0) {
return null;
}
str = value[0];
}
if (typeof value === "string") {
str = value;
}
return safeParse(parser.parse, str);
}
return {
eq: (a, b) => a === b,
...parser,
parseServerSide: parseServerSideNullable,
withDefault(defaultValue) {
return {
...this,
defaultValue,
parseServerSide(value) {
return parseServerSideNullable(value) ?? defaultValue;
}
};
},
withOptions(options) {
return {
...this,
...options
};
}
};
}
var parseAsString = createParser({
parse: (v) => v,
serialize: (v) => `${v}`
});
var parseAsInteger = createParser({
parse: (v) => {
const int = parseInt(v);
if (Number.isNaN(int)) {
return null;
}
return int;
},
serialize: (v) => Math.round(v).toFixed()
});
var parseAsIndex = createParser({
parse: (v) => {
const int = parseAsInteger.parse(v);
if (int === null) {
return null;
}
return int - 1;
},
serialize: (v) => parseAsInteger.serialize(v + 1)
});
var parseAsHex = createParser({
parse: (v) => {
const int = parseInt(v, 16);
if (Number.isNaN(int)) {
return null;
}
return int;
},
serialize: (v) => {
const hex = Math.round(v).toString(16);
return hex.padStart(hex.length + hex.length % 2, "0");
}
});
var parseAsFloat = createParser({
parse: (v) => {
const float = parseFloat(v);
if (Number.isNaN(float)) {
return null;
}
return float;
},
serialize: (v) => v.toString()
});
var parseAsBoolean = createParser({
parse: (v) => v === "true",
serialize: (v) => v ? "true" : "false"
});
function compareDates(a, b) {
return a.valueOf() === b.valueOf();
}
var parseAsTimestamp = createParser({
parse: (v) => {
const ms = parseInt(v);
if (Number.isNaN(ms)) {
return null;
}
return new Date(ms);
},
serialize: (v) => v.valueOf().toString(),
eq: compareDates
});
var parseAsIsoDateTime = createParser({
parse: (v) => {
const date = new Date(v);
if (Number.isNaN(date.valueOf())) {
return null;
}
return date;
},
serialize: (v) => v.toISOString(),
eq: compareDates
});
var parseAsIsoDate = createParser({
parse: (v) => {
const date = new Date(v.slice(0, 10));
if (Number.isNaN(date.valueOf())) {
return null;
}
return date;
},
serialize: (v) => v.toISOString().slice(0, 10),
eq: compareDates
});
function parseAsStringEnum(validValues) {
return createParser({
parse: (query) => {
const asEnum = query;
if (validValues.includes(asEnum)) {
return asEnum;
}
return null;
},
serialize: (value) => value.toString()
});
}
function parseAsStringLiteral(validValues) {
return createParser({
parse: (query) => {
const asConst = query;
if (validValues.includes(asConst)) {
return asConst;
}
return null;
},
serialize: (value) => value.toString()
});
}
function parseAsNumberLiteral(validValues) {
return createParser({
parse: (query) => {
const asConst = parseFloat(query);
if (validValues.includes(asConst)) {
return asConst;
}
return null;
},
serialize: (value) => value.toString()
});
}
function parseAsJson(runtimeParser) {
return createParser({
parse: (query) => {
try {
const obj = JSON.parse(query);
return runtimeParser(obj);
} catch {
return null;
}
},
serialize: (value) => JSON.stringify(value),
eq(a, b) {
return a === b || JSON.stringify(a) === JSON.stringify(b);
}
});
}
function parseAsArrayOf(itemParser, separator = ",") {
const itemEq = itemParser.eq ?? ((a, b) => a === b);
const encodedSeparator = encodeURIComponent(separator);
return createParser({
parse: (query) => {
if (query === "") {
return [];
}
return query.split(separator).map(
(item, index) => safeParse(
itemParser.parse,
item.replaceAll(encodedSeparator, separator),
`[${index}]`
)
).filter((value) => value !== null && value !== void 0);
},
serialize: (values) => values.map((value) => {
const str = itemParser.serialize ? itemParser.serialize(value) : String(value);
return str.replaceAll(separator, encodedSeparator);
}).join(separator),
eq(a, b) {
if (a === b) {
return true;
}
if (a.length !== b.length) {
return false;
}
return a.every((value, index) => itemEq(value, b[index]));
}
});
}
// src/url-encoding.ts
function renderQueryString(search) {
if (search.size === 0) {
return "";
}
const query = [];
for (const [key, value] of search.entries()) {
const safeKey = key.replace(/#/g, "%23").replace(/&/g, "%26").replace(/\+/g, "%2B").replace(/=/g, "%3D").replace(/\?/g, "%3F");
query.push(`${safeKey}=${encodeQueryValue(value)}`);
}
const queryString = "?" + query.join("&");
warnIfURLIsTooLong(queryString);
return queryString;
}
function encodeQueryValue(input) {
return input.replace(/%/g, "%25").replace(/\+/g, "%2B").replace(/ /g, "+").replace(/#/g, "%23").replace(/&/g, "%26").replace(/"/g, "%22").replace(/'/g, "%27").replace(/`/g, "%60").replace(/</g, "%3C").replace(/>/g, "%3E").replace(/[\x00-\x1F]/g, (char) => encodeURIComponent(char));
}
var URL_MAX_LENGTH = 2e3;
function warnIfURLIsTooLong(queryString) {
if (process.env.NODE_ENV === "production") {
return;
}
if (typeof location === "undefined") {
return;
}
const url = new URL(location.href);
url.search = queryString;
if (url.href.length > URL_MAX_LENGTH) {
console.warn(error(414));
}
}
// src/serializer.ts
function createSerializer(parsers, {
clearOnDefault = true,
urlKeys = {}
} = {}) {
function serialize(arg1BaseOrValues, arg2values = {}) {
const [base, search] = isBase(arg1BaseOrValues) ? splitBase(arg1BaseOrValues) : ["", new URLSearchParams()];
const values = isBase(arg1BaseOrValues) ? arg2values : arg1BaseOrValues;
if (values === null) {
for (const key in parsers) {
const urlKey = urlKeys[key] ?? key;
search.delete(urlKey);
}
return base + renderQueryString(search);
}
for (const key in parsers) {
const parser = parsers[key];
const value = values[key];
if (!parser || value === void 0) {
continue;
}
const urlKey = urlKeys[key] ?? key;
const isMatchingDefault = parser.defaultValue !== void 0 && (parser.eq ?? ((a, b) => a === b))(value, parser.defaultValue);
if (value === null || (parser.clearOnDefault ?? clearOnDefault ?? true) && isMatchingDefault) {
search.delete(urlKey);
} else {
search.set(urlKey, parser.serialize(value));
}
}
return base + renderQueryString(search);
}
return serialize;
}
function isBase(base) {
return typeof base === "string" || base instanceof URLSearchParams || base instanceof URL;
}
function splitBase(base) {
if (typeof base === "string") {
const [path = "", ...search] = base.split("?");
return [path, new URLSearchParams(search.join("?"))];
} else if (base instanceof URLSearchParams) {
return ["", new URLSearchParams(base)];
} else {
return [
base.origin + base.pathname,
new URLSearchParams(base.searchParams)
];
}
}
export { createLoader, createParser, createSearchParamsCache, createSerializer, parseAsArrayOf, parseAsBoolean, parseAsFloat, parseAsHex, parseAsIndex, parseAsInteger, parseAsIsoDate, parseAsIsoDateTime, parseAsJson, parseAsNumberLiteral, parseAsString, parseAsStringEnum, parseAsStringLiteral, parseAsTimestamp };