diff --git a/packages/nuqs/src/index.parsers.ts b/packages/nuqs/src/index.parsers.ts index 773160db..02c1927e 100644 --- a/packages/nuqs/src/index.parsers.ts +++ b/packages/nuqs/src/index.parsers.ts @@ -1,3 +1,3 @@ export * from './cache' export * from './parsers' -export { createSerializer } from './serialize' +export { createSerializer } from './serializer' diff --git a/packages/nuqs/src/index.ts b/packages/nuqs/src/index.ts index 0fd3dfc1..1ee6ee93 100644 --- a/packages/nuqs/src/index.ts +++ b/packages/nuqs/src/index.ts @@ -3,7 +3,7 @@ export type { HistoryOptions, Options } from './defs' export * from './deprecated' export * from './parsers' -export { createSerializer } from './serialize' +export { createSerializer } from './serializer' export { subscribeToQueryUpdates } from './sync' export type { QueryUpdateNotificationArgs, QueryUpdateSource } from './sync' export * from './useQueryState' diff --git a/packages/nuqs/src/serialize.test.ts b/packages/nuqs/src/serialize.test.ts deleted file mode 100644 index 02bb1162..00000000 --- a/packages/nuqs/src/serialize.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { parseAsBoolean, parseAsInteger, parseAsString } from './parsers' -import { createSerializer } from './serialize' - -const parsers = { - str: parseAsString, - int: parseAsInteger, - bool: parseAsBoolean -} - -describe('serialize', () => { - test('empty', () => { - const serialize = createSerializer(parsers) - const result = serialize({}) - expect(result).toBe('') - }) - test('one item', () => { - const serialize = createSerializer(parsers) - const result = serialize({ str: 'foo' }) - expect(result).toBe('?str=foo') - }) - test('several items', () => { - const serialize = createSerializer(parsers) - const result = serialize({ str: 'foo', int: 1, bool: true }) - expect(result).toBe('?str=foo&int=1&bool=true') - }) - test("null items don't show up", () => { - const serialize = createSerializer(parsers) - const result = serialize({ str: null }) - expect(result).toBe('') - }) -}) diff --git a/packages/nuqs/src/serialize.ts b/packages/nuqs/src/serialize.ts deleted file mode 100644 index 625c97ba..00000000 --- a/packages/nuqs/src/serialize.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ParserBuilder } from './parsers' -import { renderQueryString } from './url-encoding' - -type ExtractParserType = Parser extends ParserBuilder - ? ReturnType - : never - -export function createSerializer< - Parsers extends Record> ->(parsers: Parsers) { - return function serialize(values: { - [K in keyof Parsers]?: ExtractParserType - }) { - const search = new URLSearchParams() - for (const key in parsers) { - const parser = parsers[key] - const value = values[key] - if (!parser || value === undefined || value === null) { - continue - } - search.set(key, parser.serialize(value)) - } - return renderQueryString(search) - } -} diff --git a/packages/nuqs/src/serializer.test.ts b/packages/nuqs/src/serializer.test.ts new file mode 100644 index 00000000..f90ad52e --- /dev/null +++ b/packages/nuqs/src/serializer.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from 'vitest' +import { parseAsBoolean, parseAsInteger, parseAsString } from './parsers' +import { createSerializer } from './serializer' + +const parsers = { + str: parseAsString, + int: parseAsInteger, + bool: parseAsBoolean +} + +describe('serializer', () => { + test('empty', () => { + const serialize = createSerializer(parsers) + const result = serialize({}) + expect(result).toBe('') + }) + test('one item', () => { + const serialize = createSerializer(parsers) + const result = serialize({ str: 'foo' }) + expect(result).toBe('?str=foo') + }) + test('several items', () => { + const serialize = createSerializer(parsers) + const result = serialize({ str: 'foo', int: 1, bool: true }) + expect(result).toBe('?str=foo&int=1&bool=true') + }) + test("null items don't show up", () => { + const serialize = createSerializer(parsers) + const result = serialize({ str: null }) + expect(result).toBe('') + }) + test('with string base', () => { + const serialize = createSerializer(parsers) + const result = serialize('/foo', { str: 'foo' }) + expect(result).toBe('/foo?str=foo') + }) + test('with string base with search params', () => { + const serialize = createSerializer(parsers) + const result = serialize('/foo?bar=egg', { str: 'foo' }) + expect(result).toBe('/foo?bar=egg&str=foo') + }) + test('with URLSearchParams base', () => { + const serialize = createSerializer(parsers) + const search = new URLSearchParams('?bar=egg') + const result = serialize(search, { str: 'foo' }) + expect(result).toBe('?bar=egg&str=foo') + }) + test('with URL base', () => { + const serialize = createSerializer(parsers) + const url = new URL('https://example.com/path') + const result = serialize(url, { str: 'foo' }) + expect(result).toBe('https://example.com/path?str=foo') + }) + test('with URL base and search params', () => { + const serialize = createSerializer(parsers) + const url = new URL('https://example.com/path?bar=egg') + const result = serialize(url, { str: 'foo' }) + expect(result).toBe('https://example.com/path?bar=egg&str=foo') + }) + test('null deletes from base', () => { + const serialize = createSerializer(parsers) + const result = serialize('?str=bar&int=-1', { str: 'foo', int: null }) + expect(result).toBe('?str=foo') + }) +}) diff --git a/packages/nuqs/src/serializer.ts b/packages/nuqs/src/serializer.ts new file mode 100644 index 00000000..d7db4aa4 --- /dev/null +++ b/packages/nuqs/src/serializer.ts @@ -0,0 +1,60 @@ +import type { ParserBuilder } from './parsers' +import { renderQueryString } from './url-encoding' + +type ExtractParserType = Parser extends ParserBuilder + ? ReturnType + : never + +type Base = string | URLSearchParams | URL +type Values>> = Partial<{ + [K in keyof Parsers]?: ExtractParserType +}> + +export function createSerializer< + Parsers extends Record> +>(parsers: Parsers) { + function serialize(values: Values): string + function serialize(base: Base, values: Values): string + function serialize( + baseOrValues: Base | Values, + values?: Values + ) { + const [base, search] = isBase(baseOrValues) + ? splitBase(baseOrValues) + : ['', new URLSearchParams()] + const vals = isBase(baseOrValues) ? values! : baseOrValues + for (const key in parsers) { + const parser = parsers[key] + const value = vals[key] + if (!parser || value === undefined) { + continue + } + if (value === null) { + search.delete(key) + } else { + search.set(key, parser.serialize(value)) + } + } + return base + renderQueryString(search) + } + return serialize +} + +function isBase(base: any): base is Base { + return ( + typeof base === 'string' || + base instanceof URLSearchParams || + base instanceof URL + ) +} + +function splitBase(base: Base) { + if (typeof base === 'string') { + const [path = '', search] = base.split('?') + return [path, new URLSearchParams(search)] as const + } else if (base instanceof URLSearchParams) { + return ['', base] as const + } else { + return [base.origin + base.pathname, base.searchParams] as const + } +}