diff --git a/packages/docs/content/docs/server-side.mdx b/packages/docs/content/docs/server-side.mdx index 6242b39a8..d5b33625d 100644 --- a/packages/docs/content/docs/server-side.mdx +++ b/packages/docs/content/docs/server-side.mdx @@ -3,6 +3,126 @@ title: Server-Side usage description: Type-safe search params on the server --- +## Loaders + +To parse search params server-side, you can use a _loader_ function. + +You create one using the `createLoader{:ts}` function, by passing it your search params +descriptor object: + +```tsx title="searchParams.tsx" +// [!code word:createLoader] +import { parseAsFloat, createLoader } from 'nuqs/server' + +// Describe your search params, and reuse this in useQueryStates / createSerializer: +export const coordinatesSearchParams = { + latitude: parseAsFloat.withDefault(0) + longitude: parseAsFloat.withDefault(0) +} + +export const loadSearchParams = createLoader(coordinatesSearchParams) +``` + +Here, `loadSearchParams{:ts}` is a function that parses search params and returns +state variables to be consumed server-side (the same state type that [`useQueryStates{:ts}`](/docs/batching) returns). + + + +```tsx tab="Next.js (app router)" title="app/page.tsx" +// [!code word:loadSearchParams] +import { loadSearchParams } from './search-params' +import type { SearchParams } from 'nuqs/server' + +type PageProps = { + searchParams: Promise +} + +export default async function Page({ searchParams }: PageProps) { + const { latitude, longitude } = await loadSearchParams(searchParams) + return + + // Pro tip: you don't *have* to await the result. + // Pass the Promise object to children components wrapped in + // to benefit from PPR / dynamicIO and serve a static outer shell + // immediately, while streaming in the dynamic parts that depend on + // the search params when they become available. +} +``` + +```ts tab="Next.js (pages router)" title="pages/index.tsx" +// [!code word:loadSearchParams] +import type { GetServerSidePropsContext } from 'next' + +export async function getServerSideProps({ query }: GetServerSidePropsContext) { + const { latitude, longitude } = loadSearchParams(query) + // Do some server-side calculations with the coordinates + return { + props: { ... } + } +} +``` + +```tsx tab="Remix / React Router" title="app/routes/_index.tsx" +// [!code word:loadSearchParams] +export function loader({ request }: LoaderFunctionArgs) { + const { latitude, longitude } = loadSearchParams(request) // request.url works too + // Do some server-side calculations with the coordinates + return ... +} +``` + +```tsx tab="React / client-side" +// Note: you can also use this client-side (or anywhere, really), +// for a one-off parsing of non-reactive search params: + +loadSearchParams('https://example.com?latitude=42&longitude=12') +loadSearchParams(location.search) +loadSearchParams(new URL(...)) +loadSearchParams(new URLSearchParams(...)) +``` + +```tsx tab="API routes" +// App router, eg: app/api/location/route.ts +export async function GET(request: Request) { + const { latitude, longitude } = loadSearchParams(request) + // ... +} + +// Pages router, eg: pages/api/location.ts +import type { NextApiRequest, NextApiResponse } from 'next' +export default function handler( + request: NextApiRequest, + response: NextApiResponse +) { + const { latitude, longitude } = loadSearchParams(request.query) +} +``` + + + + + Loaders **don't validate** your data. If you expect positive integers + or JSON-encoded objects of a particular shape, you'll need to feed the result + of the loader to a schema validation library, like [Zod](https://zod.dev). + + Built-in validation support is coming. [Read the RFC](https://github.com/47ng/nuqs/discussions/446). + Alternatively, you can build validation into [custom parsers](/docs/parsers/making-your-own). + + +The loader function will accept the following input types to parse search params from: +- A string containing a fully qualified URL: `https://example.com/?foo=bar` +- A string containing just search params: `?foo=bar` (like `location.search{:ts}`) +- A `URL{:ts}` object +- A `URLSearchParams{:ts}` object +- A `Request{:ts}` object +- A `Record{:ts}` (eg: `{ foo: 'bar' }{:ts}`) +- A `Promise{:ts}` of any of the above, in which case it also returns a Promise. + +## Cache + This feature is available for Next.js only. @@ -11,13 +131,8 @@ If you wish to access the searchParams in a deeply nested Server Component (ie: not in the Page component), you can use `createSearchParamsCache{:ts}` to do so in a type-safe manner. - - Parsers **don't validate** your data. If you expect positive integers - or JSON-encoded objects of a particular shape, you'll need to feed the result - of the parser to a schema validation library, like [Zod](https://zod.dev). - - Built-in validation support is coming. [Read the RFC](https://github.com/47ng/nuqs/discussions/446) - +Think of it as a loader combined with a way to propagate the parsed values down +the RSC tree, like Context would on the client. ```ts title="searchParams.ts" import { diff --git a/packages/docs/public/og/server-side.jpg b/packages/docs/public/og/server-side.jpg index 8d6815b3b..b0af315ca 100644 Binary files a/packages/docs/public/og/server-side.jpg and b/packages/docs/public/og/server-side.jpg differ diff --git a/packages/e2e/next/cypress/e2e/shared/loader.cy.ts b/packages/e2e/next/cypress/e2e/shared/loader.cy.ts new file mode 100644 index 000000000..344294b99 --- /dev/null +++ b/packages/e2e/next/cypress/e2e/shared/loader.cy.ts @@ -0,0 +1,13 @@ +import { testLoader } from 'e2e-shared/specs/loader.cy' + +// In page components: + +testLoader({ path: '/app/loader', nextJsRouter: 'app' }) + +testLoader({ path: '/pages/loader', nextJsRouter: 'pages' }) + +// In API routes: + +testLoader({ path: '/api/app/loader', nextJsRouter: 'app' }) + +testLoader({ path: '/api/pages/loader', nextJsRouter: 'pages' }) diff --git a/packages/e2e/next/src/app/api/app/loader/route.ts b/packages/e2e/next/src/app/api/app/loader/route.ts new file mode 100644 index 000000000..e66ff98aa --- /dev/null +++ b/packages/e2e/next/src/app/api/app/loader/route.ts @@ -0,0 +1,25 @@ +import { loadSearchParams } from 'e2e-shared/specs/loader' +import { NextResponse } from 'next/server' + +// Needed for Next.js 14.2.0 to 14.2.3 +// (due to https://github.com/vercel/next.js/pull/66446) +export const dynamic = 'force-dynamic' + +export async function GET(request: Request) { + const { test, int } = loadSearchParams(request) + return new NextResponse( + ` + + + +
${test}
+
${int}
+ +`, + { + headers: { + 'content-type': 'text/html' + } + } + ) +} diff --git a/packages/e2e/next/src/app/app/loader/page.tsx b/packages/e2e/next/src/app/app/loader/page.tsx new file mode 100644 index 000000000..f0b78ab58 --- /dev/null +++ b/packages/e2e/next/src/app/app/loader/page.tsx @@ -0,0 +1,11 @@ +import { LoaderRenderer, loadSearchParams } from 'e2e-shared/specs/loader' +import type { SearchParams } from 'nuqs/server' + +type PageProps = { + searchParams: Promise +} + +export default async function Page({ searchParams }: PageProps) { + const serverValues = await loadSearchParams(searchParams) + return +} diff --git a/packages/e2e/next/src/app/app/push/page.tsx b/packages/e2e/next/src/app/app/push/page.tsx index 54e1e567e..1c4d818d4 100644 --- a/packages/e2e/next/src/app/app/push/page.tsx +++ b/packages/e2e/next/src/app/app/push/page.tsx @@ -1,4 +1,5 @@ import type { SearchParams } from 'nuqs/server' +import { Suspense } from 'react' import { Client } from './client' import { searchParamsCache } from './searchParams' @@ -13,7 +14,9 @@ export default async function Page({

Server side: {server}

- + + + ) } diff --git a/packages/e2e/next/src/pages/api/pages/loader.ts b/packages/e2e/next/src/pages/api/pages/loader.ts new file mode 100644 index 000000000..294aecae0 --- /dev/null +++ b/packages/e2e/next/src/pages/api/pages/loader.ts @@ -0,0 +1,22 @@ +import { loadSearchParams } from 'e2e-shared/specs/loader' +import type { NextApiRequest, NextApiResponse } from 'next' + +export default function handler( + request: NextApiRequest, + response: NextApiResponse +) { + const { test, int } = loadSearchParams(request.query) + response + .status(200) + .setHeader('content-type', 'text/html') + .send( + ` + + + +
${test}
+
${int}
+ +` + ) +} diff --git a/packages/e2e/next/src/pages/pages/loader.tsx b/packages/e2e/next/src/pages/pages/loader.tsx new file mode 100644 index 000000000..8169c3bb9 --- /dev/null +++ b/packages/e2e/next/src/pages/pages/loader.tsx @@ -0,0 +1,22 @@ +import { + type SearchParams, + LoaderRenderer, + loadSearchParams +} from 'e2e-shared/specs/loader' +import type { GetServerSidePropsContext } from 'next' + +type PageProps = { + serverValues: SearchParams +} + +export default function Page({ serverValues }: PageProps) { + return +} + +export async function getServerSideProps({ query }: GetServerSidePropsContext) { + return { + props: { + serverValues: loadSearchParams(query) + } + } +} diff --git a/packages/e2e/react-router/v6/cypress/e2e/shared/loader.cy.ts b/packages/e2e/react-router/v6/cypress/e2e/shared/loader.cy.ts new file mode 100644 index 000000000..4b7b289c4 --- /dev/null +++ b/packages/e2e/react-router/v6/cypress/e2e/shared/loader.cy.ts @@ -0,0 +1,3 @@ +import { testLoader } from 'e2e-shared/specs/loader.cy' + +testLoader({ path: '/loader' }) diff --git a/packages/e2e/react-router/v6/src/react-router.tsx b/packages/e2e/react-router/v6/src/react-router.tsx index bae460f1e..8e1540e5b 100644 --- a/packages/e2e/react-router/v6/src/react-router.tsx +++ b/packages/e2e/react-router/v6/src/react-router.tsx @@ -37,6 +37,7 @@ const router = createBrowserRouter( + )) diff --git a/packages/e2e/react-router/v6/src/routes/loader.tsx b/packages/e2e/react-router/v6/src/routes/loader.tsx new file mode 100644 index 000000000..235446f1a --- /dev/null +++ b/packages/e2e/react-router/v6/src/routes/loader.tsx @@ -0,0 +1,11 @@ +import { LoaderRenderer, loadSearchParams } from 'e2e-shared/specs/loader' +import { useLoaderData, type LoaderFunctionArgs } from 'react-router-dom' + +export function loader({ request }: LoaderFunctionArgs) { + return loadSearchParams(request) +} + +export default function Page() { + const serverValues = useLoaderData() as Awaited> + return +} diff --git a/packages/e2e/react-router/v7/app/routes.ts b/packages/e2e/react-router/v7/app/routes.ts index df91927ab..67987f620 100644 --- a/packages/e2e/react-router/v7/app/routes.ts +++ b/packages/e2e/react-router/v7/app/routes.ts @@ -18,5 +18,6 @@ export default [ route('/routing/useQueryStates/other', './routes/routing.useQueryStates.other.tsx'), route('/shallow/useQueryState', './routes/shallow.useQueryState.tsx'), route('/shallow/useQueryStates', './routes/shallow.useQueryStates.tsx'), + route('/loader', './routes/loader.tsx') ]) ] satisfies RouteConfig diff --git a/packages/e2e/react-router/v7/app/routes/loader.tsx b/packages/e2e/react-router/v7/app/routes/loader.tsx new file mode 100644 index 000000000..95505a8a9 --- /dev/null +++ b/packages/e2e/react-router/v7/app/routes/loader.tsx @@ -0,0 +1,13 @@ +import { LoaderRenderer, loadSearchParams } from 'e2e-shared/specs/loader' +import type { LoaderFunctionArgs } from 'react-router' +import type { Route } from './+types/loader' + +export function loader({ request }: LoaderFunctionArgs) { + return loadSearchParams(request) +} + +export default function Page({ + loaderData: serverValues +}: Route.ComponentProps) { + return +} diff --git a/packages/e2e/react-router/v7/cypress/e2e/shared/loader.cy.ts b/packages/e2e/react-router/v7/cypress/e2e/shared/loader.cy.ts new file mode 100644 index 000000000..4b7b289c4 --- /dev/null +++ b/packages/e2e/react-router/v7/cypress/e2e/shared/loader.cy.ts @@ -0,0 +1,3 @@ +import { testLoader } from 'e2e-shared/specs/loader.cy' + +testLoader({ path: '/loader' }) diff --git a/packages/e2e/remix/app/routes/loader.tsx b/packages/e2e/remix/app/routes/loader.tsx new file mode 100644 index 000000000..a9e846208 --- /dev/null +++ b/packages/e2e/remix/app/routes/loader.tsx @@ -0,0 +1,12 @@ +import type { LoaderFunctionArgs } from '@remix-run/node' +import { useLoaderData } from '@remix-run/react' +import { LoaderRenderer, loadSearchParams } from 'e2e-shared/specs/loader' + +export function loader({ request }: LoaderFunctionArgs) { + return loadSearchParams(request) +} + +export default function Page() { + const serverValues = useLoaderData() + return +} diff --git a/packages/e2e/remix/cypress/e2e/shared/loader.cy.ts b/packages/e2e/remix/cypress/e2e/shared/loader.cy.ts new file mode 100644 index 000000000..4b7b289c4 --- /dev/null +++ b/packages/e2e/remix/cypress/e2e/shared/loader.cy.ts @@ -0,0 +1,3 @@ +import { testLoader } from 'e2e-shared/specs/loader.cy' + +testLoader({ path: '/loader' }) diff --git a/packages/e2e/shared/specs/loader.cy.ts b/packages/e2e/shared/specs/loader.cy.ts new file mode 100644 index 000000000..cd9b3fef3 --- /dev/null +++ b/packages/e2e/shared/specs/loader.cy.ts @@ -0,0 +1,10 @@ +import { createTest } from '../create-test' + +export const testLoader = createTest('Loader', ({ path }) => { + it('loads state from the URL', () => { + cy.visit(path + '?test=pass&int=42') + cy.contains('#hydration-marker', 'hydrated').should('be.hidden') + cy.get('#test').should('have.text', 'pass') + cy.get('#int').should('have.text', '42') + }) +}) diff --git a/packages/e2e/shared/specs/loader.tsx b/packages/e2e/shared/specs/loader.tsx new file mode 100644 index 000000000..c0e6470e7 --- /dev/null +++ b/packages/e2e/shared/specs/loader.tsx @@ -0,0 +1,27 @@ +import { + createLoader, + type inferParserType, + parseAsInteger, + parseAsString +} from 'nuqs/server' + +const searchParams = { + test: parseAsString, + int: parseAsInteger +} + +export type SearchParams = inferParserType +export const loadSearchParams = createLoader(searchParams) + +type LoaderRendererProps = { + serverValues: inferParserType +} + +export function LoaderRenderer({ serverValues }: LoaderRendererProps) { + return ( + <> +
{serverValues.test}
+
{serverValues.int}
+ + ) +} diff --git a/packages/nuqs/package.json b/packages/nuqs/package.json index 47b85b2a5..d09fa0c42 100644 --- a/packages/nuqs/package.json +++ b/packages/nuqs/package.json @@ -186,7 +186,7 @@ { "name": "Server", "path": "dist/server.js", - "limit": "2 kB", + "limit": "2.5 kB", "ignore": [ "react", "next" diff --git a/packages/nuqs/src/cache.ts b/packages/nuqs/src/cache.ts index 22c2c3c78..7cc2a1306 100644 --- a/packages/nuqs/src/cache.ts +++ b/packages/nuqs/src/cache.ts @@ -2,6 +2,7 @@ import { cache } from 'react' import type { SearchParams, UrlKeys } from './defs' import { error } from './errors' +import { createLoader } from './loader' import type { inferParserType, ParserMap } from './parsers' const $input: unique symbol = Symbol('Input') @@ -10,6 +11,7 @@ export function createSearchParamsCache( parsers: Parsers, { urlKeys = {} }: { urlKeys?: UrlKeys } = {} ) { + const load = createLoader(parsers, { urlKeys }) type Keys = keyof Parsers type ParsedSearchParams = { readonly [K in Keys]: inferParserType @@ -43,11 +45,7 @@ export function createSearchParamsCache( // Different inputs in the same request - fail throw new Error(error(501)) } - for (const key in parsers) { - const parser = parsers[key]! - const urlKey = urlKeys[key] ?? key - c.searchParams[key] = parser.parseServerSide(searchParams[urlKey]) - } + c.searchParams = load(searchParams) c[$input] = searchParams return Object.freeze(c.searchParams) as ParsedSearchParams } diff --git a/packages/nuqs/src/index.server.ts b/packages/nuqs/src/index.server.ts index 3bfc48457..f94d7bbc4 100644 --- a/packages/nuqs/src/index.server.ts +++ b/packages/nuqs/src/index.server.ts @@ -1,4 +1,10 @@ export { createSearchParamsCache } from './cache' export type { HistoryOptions, Nullable, Options, SearchParams } from './defs' +export { + createLoader, + type LoaderFunction, + type LoaderInput, + type LoaderOptions +} from './loader' export * from './parsers' export { createSerializer } from './serializer' diff --git a/packages/nuqs/src/index.ts b/packages/nuqs/src/index.ts index b8bc6f515..cf144973d 100644 --- a/packages/nuqs/src/index.ts +++ b/packages/nuqs/src/index.ts @@ -1,4 +1,10 @@ export type { HistoryOptions, Nullable, Options, SearchParams } from './defs' +export { + createLoader, + type LoaderFunction, + type LoaderInput, + type LoaderOptions +} from './loader' export * from './parsers' export { createSerializer } from './serializer' export * from './useQueryState' diff --git a/packages/nuqs/src/loader.test.ts b/packages/nuqs/src/loader.test.ts new file mode 100644 index 000000000..b7e24392b --- /dev/null +++ b/packages/nuqs/src/loader.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it } from 'vitest' +import { createLoader } from './loader' +import { parseAsInteger } from './parsers' + +describe('loader', () => { + describe('sync', () => { + it('parses a URL object', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load(new URL('http://example.com/?a=1&b=2')) + expect(result).toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a Request object', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load(new Request('http://example.com/?a=1&b=2')) + expect(result).toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a URLSearchParams object', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load(new URLSearchParams('a=1&b=2')) + expect(result).toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a Record object', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load({ + a: '1', + b: '2' + }) + expect(result).toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a URL string', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load('https://example.com/?a=1&b=2') + expect(result).toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a search params string', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load('?a=1&b=2') + expect(result).toEqual({ + a: 1, + b: 2 + }) + }) + it('supports urlKeys', () => { + const load = createLoader( + { + urlKey: parseAsInteger + }, + { + urlKeys: { + urlKey: 'a' + } + } + ) + const result = load('?a=1') + expect(result).toEqual({ + urlKey: 1 + }) + }) + }) + + describe('async', () => { + it('parses a URL object', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load( + Promise.resolve(new URL('http://example.com/?a=1&b=2')) + ) + return expect(result).resolves.toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a Request object', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load( + Promise.resolve(new Request('http://example.com/?a=1&b=2')) + ) + return expect(result).resolves.toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a URLSearchParams object', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load(Promise.resolve(new URLSearchParams('a=1&b=2'))) + return expect(result).resolves.toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a Record object', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load( + Promise.resolve({ + a: '1', + b: '2' + }) + ) + return expect(result).resolves.toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a URL string', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load(Promise.resolve('https://example.com/?a=1&b=2')) + return expect(result).resolves.toEqual({ + a: 1, + b: 2 + }) + }) + it('parses a search params string', () => { + const load = createLoader({ + a: parseAsInteger, + b: parseAsInteger + }) + const result = load(Promise.resolve('?a=1&b=2')) + return expect(result).resolves.toEqual({ + a: 1, + b: 2 + }) + }) + it('supports urlKeys', () => { + const load = createLoader( + { + urlKey: parseAsInteger + }, + { + urlKeys: { + urlKey: 'a' + } + } + ) + const result = load(Promise.resolve('?a=1')) + return expect(result).resolves.toEqual({ + urlKey: 1 + }) + }) + }) +}) diff --git a/packages/nuqs/src/loader.ts b/packages/nuqs/src/loader.ts new file mode 100644 index 000000000..6deb436b5 --- /dev/null +++ b/packages/nuqs/src/loader.ts @@ -0,0 +1,124 @@ +import type { UrlKeys } from './defs' +import type { inferParserType, ParserMap } from './parsers' + +export type LoaderInput = + | URL + | Request + | URLSearchParams + | Record + | string + +export type LoaderOptions = { + urlKeys?: UrlKeys +} + +export type LoaderFunction = ReturnType< + typeof createLoader +> + +export function createLoader( + parsers: Parsers, + { urlKeys = {} }: LoaderOptions = {} +) { + type ParsedSearchParams = inferParserType + + /** + * Load & parse search params from (almost) any input. + * + * While loaders are typically used in the context of a React Router / Remix + * loader function, it can also be used in Next.js API routes or + * getServerSideProps functions, or even with the app router `searchParams` + * page prop (sync or async), if you don't need the cache behaviours. + */ + function loadSearchParams( + input: LoaderInput, + options?: LoaderOptions + ): ParsedSearchParams + + /** + * Load & parse search params from (almost) any input. + * + * While loaders are typically used in the context of a React Router / Remix + * loader function, it can also be used in Next.js API routes or + * getServerSideProps functions, or even with the app router `searchParams` + * page prop (sync or async), if you don't need the cache behaviours. + * + * Note: this async overload makes it easier to use against the `searchParams` + * page prop in Next.js 15 app router: + * + * ```tsx + * export default async function Page({ searchParams }) { + * const parsedSearchParamsPromise = loadSearchParams(searchParams) + * return ( + * // Pre-render & stream the shell immediately + * + * + * // Stream the Promise down + * + * + * + * ) + * } + * ``` + */ + function loadSearchParams( + input: Promise, + options?: LoaderOptions + ): Promise + + function loadSearchParams(input: LoaderInput | Promise) { + if (input instanceof Promise) { + return input.then(i => loadSearchParams(i)) + } + const searchParams = extractSearchParams(input) + const result = {} as any + for (const [key, parser] of Object.entries(parsers)) { + const urlKey = urlKeys[key] ?? key + const value = searchParams.get(urlKey) + result[key] = parser.parseServerSide(value ?? undefined) + } + return result + } + return loadSearchParams +} + +function extractSearchParams(input: LoaderInput): URLSearchParams { + 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 !== undefined) { + 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() +}