Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
with:
node-version-file: .node-version
cache: pnpm
- name: Install dependencies
- name: Install root workspace dependencies
run: pnpm install --ignore-scripts --frozen-lockfile --workspace-root
- name: Check monorepo with Sherif
run: pnpm run lint:sherif
Expand All @@ -42,6 +42,10 @@ jobs:
else
echo "No formatting issues found"
fi
- name: Install all dependencies for Knip
run: pnpm install --ignore-scripts --frozen-lockfile
- name: Run Knip
run: pnpm run lint:knip --workspace packages/nuqs

publint:
name: Package Linting
Expand Down
11 changes: 11 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"workspaces": {
"packages/nuqs": {
"project": ["src/**/*.ts"]
}
},
"rules": {
"optionalPeerDependencies": "off"
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
"test": "turbo run test --log-order=stream",
"prepare": "husky",
"lint": "pnpm run -w --parallel --stream '/^lint:/'",
"lint:knip": "knip",
"lint:prettier": "prettier --check ./packages/nuqs/src/**/*.ts",
"lint:sherif": "sherif"
},
"devDependencies": {
"@commitlint/config-conventional": "^19.8.1",
"commitlint": "^19.8.1",
"husky": "^9.1.7",
"knip": "^5.64.1",
"prettier": "3.6.2",
"publint": "^0.3.12",
"semantic-release": "^24.2.7",
Expand Down
5 changes: 3 additions & 2 deletions packages/nuqs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@
"test": "pnpm run --stream '/^test:/'",
"test:unit": "vitest run --typecheck",
"test:size": "size-limit",
"prepack": "./scripts/prepack.sh"
"prepack": "./scripts/prepack.sh",
"knip": "knip"
},
"dependencies": {
"@standard-schema/spec": "1.0.0"
Expand Down Expand Up @@ -175,11 +176,11 @@
"@types/node": "^24.3.0",
"@types/react": "catalog:react19",
"@types/react-dom": "catalog:react19",
"@vitejs/plugin-react": "^5.0.1",
"@vitest/coverage-v8": "^3.2.4",
"arktype": "^2.1.20",
"fast-check": "^4.2.0",
"jsdom": "^26.1.0",
"knip": "^5.64.1",
"next": "15.5.0",
"react": "catalog:react19",
"react-dom": "catalog:react19",
Expand Down
31 changes: 0 additions & 31 deletions packages/nuqs/src/adapters/lib/patch-history.test.ts

This file was deleted.

15 changes: 1 addition & 14 deletions packages/nuqs/src/adapters/lib/patch-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { debug } from '../../lib/debug'
import type { Emitter } from '../../lib/emitter'
import { error } from '../../lib/errors'
import { resetQueues, spinQueueResetMutex } from '../../lib/queues/reset'
import { getSearchParams } from '../../lib/search-params'

export type SearchParamsSyncEmitterEvents = { update: URLSearchParams }

Expand All @@ -16,20 +17,6 @@ declare global {
}
}

export function getSearchParams(url: string | URL): URLSearchParams {
if (url instanceof URL) {
return url.searchParams
}
if (url.startsWith('?')) {
return new URLSearchParams(url)
}
try {
return new URL(url, location.origin).searchParams
} catch {
return new URLSearchParams(url)
}
}

export function shouldPatchHistory(adapter: string): boolean {
if (typeof history === 'undefined') {
return false
Expand Down
5 changes: 4 additions & 1 deletion packages/nuqs/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ export {
} from './loader'
export * from './parsers'
export { createSerializer, type CreateSerializerOptions } from './serializer'
export { createStandardSchemaV1 } from './standard-schema'
export {
createStandardSchemaV1,
type CreateStandardSchemaV1Options
} from './standard-schema'
5 changes: 4 additions & 1 deletion packages/nuqs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export {
} from './loader'
export * from './parsers'
export { createSerializer, type CreateSerializerOptions } from './serializer'
export { createStandardSchemaV1 } from './standard-schema'
export {
createStandardSchemaV1,
type CreateStandardSchemaV1Options
} from './standard-schema'
export * from './useQueryState'
export * from './useQueryStates'
2 changes: 1 addition & 1 deletion packages/nuqs/src/lib/queues/throttle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export class ThrottledQueue {
if (value === null) {
search.delete(key)
} else {
search = write(value, key, search)
search = write(search, key, value)
}
}
if (processUrlSearchParams) {
Expand Down
73 changes: 73 additions & 0 deletions packages/nuqs/src/lib/search-params.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it, vi } from 'vitest'
import { getSearchParams, isAbsentFromUrl, write } from './search-params'

describe('search-params/isAbsentFromUrl', () => {
it('returns true for null', () => {
expect(isAbsentFromUrl(null)).toBe(true)
})
it('returns true for empty array', () => {
expect(isAbsentFromUrl([])).toBe(true)
})
it('returns false for string', () => {
expect(isAbsentFromUrl('a')).toBe(false)
})
it('returns false for non-empty array', () => {
expect(isAbsentFromUrl(['a'])).toBe(false)
})
})

describe('search-params/write', () => {
it('writes a string value', () => {
const params = new URLSearchParams('key=init')
const received = write(params, 'key', 'foo')
const expected = new URLSearchParams('key=foo')
expect(received).toEqual(expected)
})
it('writes an array of values', () => {
const params = new URLSearchParams('key=init')
const received = write(params, 'key', ['foo', 'bar'])
const expected = new URLSearchParams('key=foo&key=bar')
expect(received).toEqual(expected)
})
it('writes an empty array as an empty value', () => {
const params = new URLSearchParams('key=init')
const received = write(params, 'key', [])
const expected = new URLSearchParams('key=')
expect(received).toEqual(expected)
})
it('writes an empty array as an empty value when the key is not present', () => {
const params = new URLSearchParams()
const received = write(params, 'key', [])
const expected = new URLSearchParams('key=')
expect(received).toEqual(expected)
})
})

describe('search-params/getSearchParams', () => {
it('extracts search params from a URL object', () => {
const received = getSearchParams(new URL('http://example.com/?foo=bar'))
const expected = new URLSearchParams('?foo=bar')
expect(received).toEqual(expected)
})
it('extracts search params from a fully-qualified URL string', () => {
const received = getSearchParams('http://example.com/?foo=bar')
const expected = new URLSearchParams('?foo=bar')
expect(received).toEqual(expected)
})
it('extracts search params from a pathname', () => {
vi.stubGlobal('location', { origin: 'http://example.com' })
const received = getSearchParams('/?foo=bar')
const expected = new URLSearchParams('?foo=bar')
expect(received).toEqual(expected)
})
it('extracts search params from a query string', () => {
const received = getSearchParams('?foo=bar')
const expected = new URLSearchParams('?foo=bar')
expect(received).toEqual(expected)
})
it('falls back to an empty search params object for invalid inputs', () => {
const received = getSearchParams('invalid')
const expected = new URLSearchParams()
expect(received).toEqual(expected)
})
})
18 changes: 16 additions & 2 deletions packages/nuqs/src/lib/search-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export function isAbsentFromUrl(query: Query | null): query is null | [] {
}

export function write(
serialized: Query,
searchParams: URLSearchParams,
key: string,
searchParams: URLSearchParams
serialized: Query
): URLSearchParams {
if (typeof serialized === 'string') {
searchParams.set(key, serialized)
Expand All @@ -25,3 +25,17 @@ export function write(
}
return searchParams
}

export function getSearchParams(url: string | URL): URLSearchParams {
if (url instanceof URL) {
return url.searchParams
}
if (url.startsWith('?')) {
return new URLSearchParams(url)
}
try {
return new URL(url, location.origin).searchParams
} catch {
return new URLSearchParams(url)
}
}
4 changes: 2 additions & 2 deletions packages/nuqs/src/lib/url-encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ export function encodeQueryValue(input: string): string {
}

// Note: change error documentation (NUQS-414) when changing this value.
export const URL_MAX_LENGTH = 2000
const URL_MAX_LENGTH = 2000

export function warnIfURLIsTooLong(queryString: string): void {
function warnIfURLIsTooLong(queryString: string): void {
if (process.env.NODE_ENV === 'production') {
return
}
Expand Down
4 changes: 2 additions & 2 deletions packages/nuqs/src/serializer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Nullable, Options, UrlKeys } from './defs'
import { write } from './lib/search-params'
import { renderQueryString } from './lib/url-encoding'
import type { inferParserType, ParserMap } from './parsers'
import { write } from './lib/search-params'

type Base = string | URLSearchParams | URL

Expand Down Expand Up @@ -99,7 +99,7 @@ export function createSerializer<
search.delete(urlKey)
} else {
const serialized = parser.serialize(value)
search = write(serialized, urlKey, search)
search = write(search, urlKey, serialized)
}
}
if (processUrlSearchParams) {
Expand Down
8 changes: 8 additions & 0 deletions packages/nuqs/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ const config: ViteUserConfig = defineConfig({
deps: {
inline: ['vitest-package-exports']
}
},
coverage: {
provider: 'v8',
exclude: [
'./src/adapters/**', // adapters are tested in e2e tests
'./tests/**.test-d.ts', // type tests don't generate coverage
'./**/*.d.ts' // neither do type definitions
]
}
}
})
Expand Down
Loading
Loading