Skip to content
23 changes: 3 additions & 20 deletions packages/router-core/src/qss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,6 @@ export function encode(
return result.toString()
}

/**
* Converts a string value to its appropriate type (string, number, boolean).
* @param mix - The string value to convert.
* @returns The converted value.
* @example
* // Example input: toValue("123")
* // Expected output: 123
*/
/** Convert a string into a primitive boolean/number when possible. */
function toValue(str: unknown) {
if (!str) return ''

if (str === 'false') return false
if (str === 'true') return true
return +str * 0 === 0 && +str + '' === str ? +str : str
}

/**
* Decodes a query string into an object.
* @param str - The query string to decode.
Expand All @@ -73,11 +56,11 @@ export function decode(str: any): any {
for (const [key, value] of searchParams.entries()) {
const previousValue = result[key]
if (previousValue == null) {
result[key] = toValue(value)
result[key] = value
} else if (Array.isArray(previousValue)) {
previousValue.push(toValue(value))
previousValue.push(value)
} else {
result[key] = [previousValue, toValue(value)]
result[key] = [previousValue, value]
}
}

Expand Down
11 changes: 8 additions & 3 deletions packages/router-core/src/searchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export function parseSearchWith(parser: (str: string) => any) {
const value = query[key]
if (typeof value === 'string') {
try {
query[key] = parser(value)
const parsed = parser(value)
if (parsed && typeof parsed === 'object') {
query[key] = parsed
}
} catch (_err) {
// silent
}
Expand Down Expand Up @@ -73,8 +76,10 @@ export function stringifySearchWith(
try {
// Check if it's a valid parseable string.
// If it is, then stringify it again.
parser(val)
return stringify(val)
const parsed = parser(val)
if (parsed && typeof parsed === 'object') {
return stringify(val)
}
} catch (_err) {
// silent
}
Expand Down
2 changes: 1 addition & 1 deletion packages/router-core/tests/qss.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('decode function', () => {
it('should handle decoding a top-level key with a special character', () => {
const queryString = 'foo%3Dbar=1'
const decodedObj = decode(queryString)
expect(decodedObj).toEqual({ 'foo=bar': 1 })
expect(decodedObj).toEqual({ 'foo=bar': '1' })
})

it('should handle decoding a top-level key with a special character and without a value', () => {
Expand Down
77 changes: 39 additions & 38 deletions packages/router-core/tests/searchParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,44 @@ import { describe, expect, test } from 'vitest'
import { defaultParseSearch, defaultStringifySearch } from '../src'

describe('Search Params serialization and deserialization', () => {
/*
* JSON-compatible objects can be serialized into a string,
* and then deserialized back into the original object.
*/
test.each([
[{}, ''],
[{ foo: '' }, '?foo='],
[{ foo: 'bar' }, '?foo=bar'],
[{ foo: 'bar baz' }, '?foo=bar+baz'],
[{ foo: 123 }, '?foo=123'],
[{ foo: '123' }, '?foo=%22123%22'],
[{ foo: true }, '?foo=true'],
[{ foo: 'true' }, '?foo=%22true%22'],
[{ foo: null }, '?foo=null'],
[{ foo: 'null' }, '?foo=%22null%22'],
[{ foo: 'undefined' }, '?foo=undefined'],
[{ foo: {} }, '?foo=%7B%7D'],
[{ foo: '{}' }, '?foo=%22%7B%7D%22'],
[{ foo: [] }, '?foo=%5B%5D'],
[{ foo: '[]' }, '?foo=%22%5B%5D%22'],
[{ foo: [1, 2, 3] }, '?foo=%5B1%2C2%2C3%5D'],
[{ foo: '1,2,3' }, '?foo=1%2C2%2C3'],
[{ foo: { bar: 'baz' } }, '?foo=%7B%22bar%22%3A%22baz%22%7D'],
[{ 0: 1 }, '?0=1'],
[{ 'foo=bar': 1 }, '?foo%3Dbar=1'],
[{ '{}': 1 }, '?%7B%7D=1'],
[{ '': 1 }, '?=1'],
[{ '=': '=' }, '?%3D=%3D'],
[{ '=': '', '': '=' }, '?%3D=&=%3D'],
[{ 'foo=2&bar': 3 }, '?foo%3D2%26bar=3'],
[{ 'foo?': 1 }, '?foo%3F=1'],
[{ foo: 'bar=' }, '?foo=bar%3D'],
[{ foo: '2&bar=3' }, '?foo=2%26bar%3D3'],
Comment thread
adbjo marked this conversation as resolved.
])('isomorphism %j', (input, expected) => {
const str = defaultStringifySearch(input)
expect(str).toEqual(expected)
expect(defaultParseSearch(str)).toEqual(input)
['', {}],
['?foo=', { foo: '' }],
['?foo=bar', { foo: 'bar' }],
['?foo=%22bar%22', { foo: '"bar"' }],
['?foo=bar+baz', { foo: 'bar baz' }],
['?foo=%22bar+baz%22', { foo: '"bar baz"' }],
['?foo=123', { foo: '123' }],
['?foo=%22123%22', { foo: '"123"' }],
['?foo=true', { foo: 'true' }],
['?foo=%22true%22', { foo: '"true"' }],
['?foo=null', { foo: 'null' }],
['?foo=%22null%22', { foo: '"null"' }],
['?foo=undefined', { foo: 'undefined' }],
['?foo=%22undefined%22', { foo: '"undefined"' }],
['?foo=%7B%7D', { foo: {} }],
['?foo=%22%7B%7D%22', { foo: '"{}"' }],
['?foo=%5B%5D', { foo: [] }],
['?foo=%22%5B%5D%22', { foo: '"[]"' }],
['?foo=%5B1%2C2%2C3%5D', { foo: [1, 2, 3] }],
['?foo=1%2C2%2C3', { foo: '1,2,3' }],
['?foo=%7B%22bar%22%3A%22baz%22%7D', { foo: { bar: 'baz' } }],
['?0=1', { 0: '1' }],
['?foo%3Dbar=1', { 'foo=bar': '1' }],
['?%7B%7D=1', { '{}': '1' }],
['?=1', { '': '1' }],
['?%3D=%3D', { '=': '=' }],
['?%3D=&=%3D', { '=': '', '': '=' }],
['?foo%3D2%26bar=3', { 'foo=2&bar': '3' }],
['?foo%3F=1', { 'foo?': '1' }],
['?foo=bar%3D', { foo: 'bar=' }],
['?foo=2%26bar%3D3', { foo: '2&bar=3' }],
['?foo=1000000000000001110', { foo: '1000000000000001110' }],
])('isomorphism %j', (input, expectedObj) => {
const parsed = defaultParseSearch(input)
expect(parsed).toEqual(expectedObj)
const str = defaultStringifySearch(parsed)
expect(str).toEqual(input)
})

test('undefined values are removed during stringification', () => {
Expand Down Expand Up @@ -67,8 +68,8 @@ describe('Search Params serialization and deserialization', () => {
['?foo=[]', { foo: [] }],
['?foo=1,2,3', { foo: '1,2,3' }],
['?foo={"bar":"baz"}', { foo: { bar: 'baz' } }],
['?foo=1&foo=2', { foo: [1, 2] }],
['?foo=""', { foo: '' }],
['?foo=1&foo=2', { foo: ['1', '2'] }],
['?foo=""', { foo: '""' }],
['?foo=""""', { foo: '""""' }],
['?foo=()', { foo: '()' }],
['?foo=[{}]', { foo: [{}] }],
Expand Down