diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 62dce2e51..722abda94 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -155,9 +155,15 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} e2e-react-router: - name: E2E (react-router) + name: E2E (react-router ${{ matrix.react-router-version }}) runs-on: ubuntu-24.04 needs: [ci-core] + strategy: + fail-fast: false + matrix: + react-router-version: + - 'v6' + - 'v7' steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 @@ -168,7 +174,7 @@ jobs: - name: Install dependencies run: pnpm install - name: Run tests - run: pnpm run test ${{ github.event_name == 'workflow_dispatch' && '--force' || '' }} --filter e2e-react-router + run: pnpm run test ${{ github.event_name == 'workflow_dispatch' && '--force' || '' }} --filter e2e-react-router-${{ matrix.react-router-version }} env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} @@ -177,14 +183,14 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 if: failure() with: - path: packages/e2e/react-router/cypress/screenshots - name: ci-react-router + path: packages/e2e/react-router/${{ matrix.react-router-version }}/cypress/screenshots + name: ci-react-router-${{ matrix.react-router-version }} - uses: 47ng/actions-slack-notify@main name: Notify on Slack if: always() with: status: ${{ job.status }} - jobName: react-router + jobName: react-router-${{ matrix.react-router-version }} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfc8bcb13..b46a034f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,11 @@ When running `next dev`, this will: - Build the library and watch for changes using [`tsup`](https://tsup.egoist.dev/) - Start the docs app, which will be available at . - Start the end-to-end test benches: - - Next.js: http://localhost:3001 - - Remix: http://localhost:3002 - - React Router: http://localhost:3003 - - React SPA: http://localhost:3004 + - http://localhost:3001 - Next.js + - http://localhost:3002 - React SPA + - http://localhost:3003 - Remix + - http://localhost:3006 - React Router v6 + - http://localhost:3007 - React Router v7 ## Testing diff --git a/packages/docs/content/docs/adapters.mdx b/packages/docs/content/docs/adapters.mdx index eef47619e..0e69f30df 100644 --- a/packages/docs/content/docs/adapters.mdx +++ b/packages/docs/content/docs/adapters.mdx @@ -126,6 +126,8 @@ export function ReactRouter() { } ``` +**Note**: If you are using react-router v7, please import the `NuqsAdapter{:ts}` from `nuqs/adapters/react-router/v7` + ## Testing diff --git a/packages/e2e/react-router/package.json b/packages/e2e/react-router/package.json index 6b14cedbd..94f4e755d 100644 --- a/packages/e2e/react-router/package.json +++ b/packages/e2e/react-router/package.json @@ -1,35 +1,5 @@ { "name": "e2e-react-router", - "private": true, "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite --port 4000", - "build": "tsc -b && vite build", - "preview": "vite preview", - "test": "pnpm run '/^test:/'", - "test:unit": "vitest", - "test:e2e": "echo 'todo: Implement e2e tests'" - }, - "dependencies": { - "nuqs": "workspace:*", - "react": "catalog:react19", - "react-dom": "catalog:react19", - "react-router-dom": "^6.28.0" - }, - "devDependencies": { - "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.0.1", - "@testing-library/user-event": "^14.5.2", - "@types/node": "^22.9.0", - "@types/react": "catalog:react19", - "@types/react-dom": "catalog:react19", - "@vitejs/plugin-react": "^4.3.3", - "globals": "^15.12.0", - "jsdom": "^25.0.1", - "typescript": "^5.6.3", - "vite": "^5.4.11", - "vitest": "^2.1.5" - } + "private": true } diff --git a/packages/e2e/react-router/.gitignore b/packages/e2e/react-router/v6/.gitignore similarity index 100% rename from packages/e2e/react-router/.gitignore rename to packages/e2e/react-router/v6/.gitignore diff --git a/packages/e2e/react-router/README.md b/packages/e2e/react-router/v6/README.md similarity index 100% rename from packages/e2e/react-router/README.md rename to packages/e2e/react-router/v6/README.md diff --git a/packages/e2e/react-router/index.html b/packages/e2e/react-router/v6/index.html similarity index 100% rename from packages/e2e/react-router/index.html rename to packages/e2e/react-router/v6/index.html diff --git a/packages/e2e/react-router/v6/package.json b/packages/e2e/react-router/v6/package.json new file mode 100644 index 000000000..f37402338 --- /dev/null +++ b/packages/e2e/react-router/v6/package.json @@ -0,0 +1,35 @@ +{ + "name": "e2e-react-router-v6", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3006", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "pnpm run '/^test:/'", + "test:unit": "vitest", + "test:e2e": "echo 'todo: Implement e2e tests'" + }, + "dependencies": { + "nuqs": "workspace:*", + "react": "catalog:react19", + "react-dom": "catalog:react19", + "react-router-dom": "^6.28.0" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/node": "^22.9.0", + "@types/react": "catalog:react19", + "@types/react-dom": "catalog:react19", + "@vitejs/plugin-react": "^4.3.3", + "globals": "^15.12.0", + "jsdom": "^25.0.1", + "typescript": "^5.6.3", + "vite": "^5.4.11", + "vitest": "^2.1.5" + } +} diff --git a/packages/e2e/react-router/src/App.tsx b/packages/e2e/react-router/v6/src/App.tsx similarity index 100% rename from packages/e2e/react-router/src/App.tsx rename to packages/e2e/react-router/v6/src/App.tsx diff --git a/packages/e2e/react-router/src/components/counter-button.test.tsx b/packages/e2e/react-router/v6/src/components/counter-button.test.tsx similarity index 100% rename from packages/e2e/react-router/src/components/counter-button.test.tsx rename to packages/e2e/react-router/v6/src/components/counter-button.test.tsx diff --git a/packages/e2e/react-router/src/components/counter-button.tsx b/packages/e2e/react-router/v6/src/components/counter-button.tsx similarity index 100% rename from packages/e2e/react-router/src/components/counter-button.tsx rename to packages/e2e/react-router/v6/src/components/counter-button.tsx diff --git a/packages/e2e/react-router/src/components/search-input.test.tsx b/packages/e2e/react-router/v6/src/components/search-input.test.tsx similarity index 100% rename from packages/e2e/react-router/src/components/search-input.test.tsx rename to packages/e2e/react-router/v6/src/components/search-input.test.tsx diff --git a/packages/e2e/react-router/src/components/search-input.tsx b/packages/e2e/react-router/v6/src/components/search-input.tsx similarity index 100% rename from packages/e2e/react-router/src/components/search-input.tsx rename to packages/e2e/react-router/v6/src/components/search-input.tsx diff --git a/packages/e2e/react-router/src/main.tsx b/packages/e2e/react-router/v6/src/main.tsx similarity index 100% rename from packages/e2e/react-router/src/main.tsx rename to packages/e2e/react-router/v6/src/main.tsx diff --git a/packages/e2e/react-router/src/react-router.tsx b/packages/e2e/react-router/v6/src/react-router.tsx similarity index 83% rename from packages/e2e/react-router/src/react-router.tsx rename to packages/e2e/react-router/v6/src/react-router.tsx index 6b8d285eb..e374cb59d 100644 --- a/packages/e2e/react-router/src/react-router.tsx +++ b/packages/e2e/react-router/v6/src/react-router.tsx @@ -1,4 +1,4 @@ -import { NuqsAdapter } from 'nuqs/adapters/react-router' +import { NuqsAdapter } from 'nuqs/adapters/react-router/v6' import { createBrowserRouter, RouterProvider } from 'react-router-dom' import App from './App' diff --git a/packages/e2e/react-router/src/vite-env.d.ts b/packages/e2e/react-router/v6/src/vite-env.d.ts similarity index 100% rename from packages/e2e/react-router/src/vite-env.d.ts rename to packages/e2e/react-router/v6/src/vite-env.d.ts diff --git a/packages/e2e/react-router/tsconfig.app.json b/packages/e2e/react-router/v6/tsconfig.app.json similarity index 100% rename from packages/e2e/react-router/tsconfig.app.json rename to packages/e2e/react-router/v6/tsconfig.app.json diff --git a/packages/e2e/react-router/tsconfig.json b/packages/e2e/react-router/v6/tsconfig.json similarity index 100% rename from packages/e2e/react-router/tsconfig.json rename to packages/e2e/react-router/v6/tsconfig.json diff --git a/packages/e2e/react-router/tsconfig.node.json b/packages/e2e/react-router/v6/tsconfig.node.json similarity index 100% rename from packages/e2e/react-router/tsconfig.node.json rename to packages/e2e/react-router/v6/tsconfig.node.json diff --git a/packages/e2e/react-router/vite.config.ts b/packages/e2e/react-router/v6/vite.config.ts similarity index 100% rename from packages/e2e/react-router/vite.config.ts rename to packages/e2e/react-router/v6/vite.config.ts diff --git a/packages/e2e/react-router/vitest.setup.ts b/packages/e2e/react-router/v6/vitest.setup.ts similarity index 100% rename from packages/e2e/react-router/vitest.setup.ts rename to packages/e2e/react-router/v6/vitest.setup.ts diff --git a/packages/e2e/react-router/v7/.gitignore b/packages/e2e/react-router/v7/.gitignore new file mode 100644 index 000000000..9b7c041f9 --- /dev/null +++ b/packages/e2e/react-router/v7/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/node_modules/ + +# React Router +/.react-router/ +/build/ diff --git a/packages/e2e/react-router/v7/README.md b/packages/e2e/react-router/v7/README.md new file mode 100644 index 000000000..e0d20664e --- /dev/null +++ b/packages/e2e/react-router/v7/README.md @@ -0,0 +1,100 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications using React Router. + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +### Docker Deployment + +This template includes three Dockerfiles optimized for different package managers: + +- `Dockerfile` - for npm +- `Dockerfile.pnpm` - for pnpm +- `Dockerfile.bun` - for bun + +To build and run using Docker: + +```bash +# For npm +docker build -t my-app . + +# For pnpm +docker build -f Dockerfile.pnpm -t my-app . + +# For bun +docker build -f Dockerfile.bun -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is production-ready. + +Make sure to deploy the output of `npm run build` + +``` +├── package.json +├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/packages/e2e/react-router/v7/app/root.tsx b/packages/e2e/react-router/v7/app/root.tsx new file mode 100644 index 000000000..9344e74ae --- /dev/null +++ b/packages/e2e/react-router/v7/app/root.tsx @@ -0,0 +1,66 @@ +import { NuqsAdapter } from 'nuqs/adapters/react-router/v7' +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration +} from 'react-router' + +import type { Route } from './+types/root' + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ) +} + +export default function App() { + return ( + + + + ) +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = 'Oops!' + let details = 'An unexpected error occurred.' + let stack: string | undefined + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error' + details = + error.status === 404 + ? 'The requested page could not be found.' + : error.statusText || details + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message + stack = error.stack + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ) +} diff --git a/packages/e2e/react-router/v7/app/routes.ts b/packages/e2e/react-router/v7/app/routes.ts new file mode 100644 index 000000000..102b40258 --- /dev/null +++ b/packages/e2e/react-router/v7/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from "@react-router/dev/routes"; + +export default [index("routes/home.tsx")] satisfies RouteConfig; diff --git a/packages/e2e/react-router/v7/app/routes/home.tsx b/packages/e2e/react-router/v7/app/routes/home.tsx new file mode 100644 index 000000000..a584b0153 --- /dev/null +++ b/packages/e2e/react-router/v7/app/routes/home.tsx @@ -0,0 +1,12 @@ +import type { Route } from './+types/home' + +export function meta({}: Route.MetaArgs) { + return [ + { title: 'New React Router App' }, + { name: 'description', content: 'Welcome to React Router!' } + ] +} + +export default function Home() { + return

Hello, RRv7

+} diff --git a/packages/e2e/react-router/v7/package.json b/packages/e2e/react-router/v7/package.json new file mode 100644 index 000000000..74c80fd61 --- /dev/null +++ b/packages/e2e/react-router/v7/package.json @@ -0,0 +1,29 @@ +{ + "name": "e2e-react-router-v7", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "react-router dev --port 3007", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc" + }, + "dependencies": { + "@react-router/node": "^7.0.2", + "@react-router/serve": "^7.0.2", + "isbot": "^5.1.17", + "nuqs": "workspace:*", + "react": "catalog:react19", + "react-dom": "catalog:react19", + "react-router": "^7.0.2" + }, + "devDependencies": { + "@react-router/dev": "^7.0.2", + "@types/node": "^20", + "@types/react": "catalog:react19", + "@types/react-dom": "catalog:react19", + "typescript": "^5.6.3", + "vite": "^5.4.11", + "vite-tsconfig-paths": "^5.1.2" + } +} diff --git a/packages/e2e/react-router/v7/public/favicon.ico b/packages/e2e/react-router/v7/public/favicon.ico new file mode 100644 index 000000000..8830cf682 Binary files /dev/null and b/packages/e2e/react-router/v7/public/favicon.ico differ diff --git a/packages/e2e/react-router/v7/react-router.config.ts b/packages/e2e/react-router/v7/react-router.config.ts new file mode 100644 index 000000000..6ff16f917 --- /dev/null +++ b/packages/e2e/react-router/v7/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/packages/e2e/react-router/v7/tsconfig.json b/packages/e2e/react-router/v7/tsconfig.json new file mode 100644 index 000000000..dc391a45f --- /dev/null +++ b/packages/e2e/react-router/v7/tsconfig.json @@ -0,0 +1,27 @@ +{ + "include": [ + "**/*", + "**/.server/**/*", + "**/.client/**/*", + ".react-router/types/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/packages/e2e/react-router/v7/vite.config.ts b/packages/e2e/react-router/v7/vite.config.ts new file mode 100644 index 000000000..bb848d388 --- /dev/null +++ b/packages/e2e/react-router/v7/vite.config.ts @@ -0,0 +1,7 @@ +import { reactRouter } from '@react-router/dev/vite' +import { defineConfig } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + plugins: [reactRouter(), tsconfigPaths()] +}) diff --git a/packages/e2e/react/package.json b/packages/e2e/react/package.json index cb54c99fc..3cc77dd11 100644 --- a/packages/e2e/react/package.json +++ b/packages/e2e/react/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite --port 4000", + "dev": "vite --port 3002", "build": "tsc -b && vite build", "preview": "vite preview", "test": "pnpm run '/^test:/'", diff --git a/packages/e2e/remix/package.json b/packages/e2e/remix/package.json index 8d4e4db8d..972726899 100644 --- a/packages/e2e/remix/package.json +++ b/packages/e2e/remix/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "remix vite:build", - "dev": "remix vite:dev --port 4001", + "dev": "remix vite:dev --port 3003", "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "start": "remix-serve ./build/server/index.js", "test": "pnpm run '/^test:/'", diff --git a/packages/nuqs/adapters/react-router.d.ts b/packages/nuqs/adapters/react-router.d.ts index 2d6d92809..c69e44506 100644 --- a/packages/nuqs/adapters/react-router.d.ts +++ b/packages/nuqs/adapters/react-router.d.ts @@ -3,5 +3,8 @@ // Other module resolutions strategies will look for the `exports` in `package.json`, // but with `node`, TypeScript will look for a .d.ts file with that name at the // root of the package. +// +// Note: this default react-router adapter is for react-router v6. +// If you are using react-router v7, please import from `nuqs/adapters/react-router/v7` export * from '../dist/adapters/react-router' diff --git a/packages/nuqs/adapters/react-router/v6.d.ts b/packages/nuqs/adapters/react-router/v6.d.ts new file mode 100644 index 000000000..4986652a7 --- /dev/null +++ b/packages/nuqs/adapters/react-router/v6.d.ts @@ -0,0 +1,7 @@ +// This file is needed for projects that have `moduleResolution` set to `node` +// in their tsconfig.json to be able to `import {} from 'nuqs/adapters/react-router/v6'`. +// Other module resolutions strategies will look for the `exports` in `package.json`, +// but with `node`, TypeScript will look for a .d.ts file with that name at the +// root of the package. + +export * from '../../dist/adapters/react-router/v6' diff --git a/packages/nuqs/adapters/react-router/v7.d.ts b/packages/nuqs/adapters/react-router/v7.d.ts new file mode 100644 index 000000000..4bce83e91 --- /dev/null +++ b/packages/nuqs/adapters/react-router/v7.d.ts @@ -0,0 +1,7 @@ +// This file is needed for projects that have `moduleResolution` set to `node` +// in their tsconfig.json to be able to `import {} from 'nuqs/adapters/react-router/v7'`. +// Other module resolutions strategies will look for the `exports` in `package.json`, +// but with `node`, TypeScript will look for a .d.ts file with that name at the +// root of the package. + +export * from '../../dist/adapters/react-router/v7' diff --git a/packages/nuqs/package.json b/packages/nuqs/package.json index 329d1e5c6..1f353b21e 100644 --- a/packages/nuqs/package.json +++ b/packages/nuqs/package.json @@ -88,6 +88,16 @@ "import": "./dist/adapters/react-router.js", "require": "./esm-only.cjs" }, + "./adapters/react-router/v6": { + "types": "./dist/adapters/react-router/v6.d.ts", + "import": "./dist/adapters/react-router/v6.js", + "require": "./esm-only.cjs" + }, + "./adapters/react-router/v7": { + "types": "./dist/adapters/react-router/v7.d.ts", + "import": "./dist/adapters/react-router/v7.js", + "require": "./esm-only.cjs" + }, "./adapters/custom": { "types": "./dist/adapters/custom.d.ts", "import": "./dist/adapters/custom.js", @@ -117,6 +127,7 @@ "@remix-run/react": ">=2", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", + "react-router": ">=7", "react-router-dom": ">=6" }, "peerDependenciesMeta": { @@ -126,6 +137,9 @@ "next": { "optional": true }, + "react-router": { + "optional": true + }, "react-router-dom": { "optional": true } diff --git a/packages/nuqs/src/adapters/react-router.ts b/packages/nuqs/src/adapters/react-router.ts index 2f4ac2f4e..a2c9041d8 100644 --- a/packages/nuqs/src/adapters/react-router.ts +++ b/packages/nuqs/src/adapters/react-router.ts @@ -1,9 +1,12 @@ +// Note: this default react-router adapter is for react-router v6. +// If you are using react-router v7, please import from `nuqs/adapters/react-router/v7` + import { useNavigate, useSearchParams } from 'react-router-dom' import { renderQueryString } from '../url-encoding' import type { AdapterOptions } from './defs' import { createAdapterProvider } from './internal.context' -function useNuqsReactRouterAdapter() { +function useNuqsReactRouterV6Adapter() { const navigate = useNavigate() const [searchParams] = useSearchParams() const updateUrl = (search: URLSearchParams, options: AdapterOptions) => { @@ -23,4 +26,4 @@ function useNuqsReactRouterAdapter() { } } -export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterAdapter) +export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV6Adapter) diff --git a/packages/nuqs/src/adapters/react-router/v6.ts b/packages/nuqs/src/adapters/react-router/v6.ts new file mode 100644 index 000000000..14c544c49 --- /dev/null +++ b/packages/nuqs/src/adapters/react-router/v6.ts @@ -0,0 +1,26 @@ +import { useNavigate, useSearchParams } from 'react-router-dom' +import { renderQueryString } from '../../url-encoding' +import type { AdapterOptions } from '../defs' +import { createAdapterProvider } from '../internal.context' + +function useNuqsReactRouterV6Adapter() { + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const updateUrl = (search: URLSearchParams, options: AdapterOptions) => { + navigate( + { + search: renderQueryString(search) + }, + { + replace: options.history === 'replace', + preventScrollReset: !options.scroll + } + ) + } + return { + searchParams, + updateUrl + } +} + +export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV6Adapter) diff --git a/packages/nuqs/src/adapters/react-router/v7.ts b/packages/nuqs/src/adapters/react-router/v7.ts new file mode 100644 index 000000000..cb5d2bae8 --- /dev/null +++ b/packages/nuqs/src/adapters/react-router/v7.ts @@ -0,0 +1,26 @@ +import { useNavigate, useSearchParams } from 'react-router' +import { renderQueryString } from '../../url-encoding' +import type { AdapterOptions } from '../defs' +import { createAdapterProvider } from '../internal.context' + +function useNuqsReactRouterV7Adapter() { + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const updateUrl = (search: URLSearchParams, options: AdapterOptions) => { + navigate( + { + search: renderQueryString(search) + }, + { + replace: options.history === 'replace', + preventScrollReset: !options.scroll + } + ) + } + return { + searchParams, + updateUrl + } +} + +export const NuqsAdapter = createAdapterProvider(useNuqsReactRouterV7Adapter) diff --git a/packages/nuqs/tsup.config.ts b/packages/nuqs/tsup.config.ts index f2374ff9b..2120fa7fc 100644 --- a/packages/nuqs/tsup.config.ts +++ b/packages/nuqs/tsup.config.ts @@ -7,7 +7,13 @@ const commonConfig = { format: ['esm'], experimentalDts: true, outDir: 'dist', - external: ['next', 'react', '@remix-run/react', 'react-router-dom'], + external: [ + 'next', + 'react', + '@remix-run/react', + 'react-router-dom', + 'react-router' + ], splitting: true, treeshake: true, tsconfig: 'tsconfig.build.json' @@ -22,6 +28,8 @@ const entrypoints = { 'adapters/next/pages': 'src/adapters/next/pages.ts', 'adapters/remix': 'src/adapters/remix.ts', 'adapters/react-router': 'src/adapters/react-router.ts', + 'adapters/react-router/v6': 'src/adapters/react-router/v6.ts', + 'adapters/react-router/v7': 'src/adapters/react-router/v7.ts', 'adapters/custom': 'src/adapters/custom.ts', 'adapters/testing': 'src/adapters/testing.ts' }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60ea7f7b0..59676037d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,11 +305,13 @@ importers: specifier: ^2.1.5 version: 2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.34.1) - packages/e2e/react-router: + packages/e2e/react-router: {} + + packages/e2e/react-router/v6: dependencies: nuqs: specifier: workspace:* - version: link:../../nuqs + version: link:../../../nuqs react: specifier: catalog:react19 version: 19.0.0 @@ -360,6 +362,52 @@ importers: specifier: ^2.1.5 version: 2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.34.1) + packages/e2e/react-router/v7: + dependencies: + '@react-router/node': + specifier: ^7.0.2 + version: 7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3) + '@react-router/serve': + specifier: ^7.0.2 + version: 7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3) + isbot: + specifier: ^5.1.17 + version: 5.1.17 + nuqs: + specifier: workspace:* + version: link:../../../nuqs + react: + specifier: catalog:react19 + version: 19.0.0 + react-dom: + specifier: catalog:react19 + version: 19.0.0(react@19.0.0) + react-router: + specifier: ^7.0.2 + version: 7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + devDependencies: + '@react-router/dev': + specifier: ^7.0.2 + version: 7.0.2(@react-router/serve@7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3))(@types/node@20.17.9)(lightningcss@1.27.0)(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(terser@5.34.1)(typescript@5.6.3)(vite@5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1)) + '@types/node': + specifier: ^20 + version: 20.17.9 + '@types/react': + specifier: catalog:react19 + version: 19.0.0 + '@types/react-dom': + specifier: catalog:react19 + version: 19.0.0 + typescript: + specifier: ^5.6.3 + version: 5.6.3 + vite: + specifier: ^5.4.11 + version: 5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1) + vite-tsconfig-paths: + specifier: ^5.1.2 + version: 5.1.2(typescript@5.6.3)(vite@5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1)) + packages/e2e/remix: dependencies: '@remix-run/node': @@ -441,6 +489,9 @@ importers: mitt: specifier: ^3.0.1 version: 3.0.1 + react-router: + specifier: '>=7' + version: 7.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) devDependencies: '@microsoft/api-extractor': specifier: ^7.47.11 @@ -1765,6 +1816,9 @@ packages: '@microsoft/tsdoc@0.15.0': resolution: {integrity: sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==} + '@mjackson/node-fetch-server@0.2.0': + resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==} + '@next/env@15.0.3': resolution: {integrity: sha512-t9Xy32pjNOvVn2AS+Utt6VmyrshbpfUMhIjFO60gI58deSo/KgLOp31XZ4O+kY/Is8WAGYwA5gR7kOb1eORDBA==} @@ -2616,6 +2670,52 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + '@react-router/dev@7.0.2': + resolution: {integrity: sha512-uT9OVTGJAtOHGSvAlES4Y2HLqLQ7pENffUhlS7Is7eEVWQeTfZei/1RXTnNwpLbwAuDEf7DHbINDeVLDdjP92w==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@react-router/serve': ^7.0.2 + react-router: ^7.0.2 + typescript: ^5.1.0 + vite: ^5.1.0 + wrangler: ^3.28.2 + peerDependenciesMeta: + '@react-router/serve': + optional: true + typescript: + optional: true + wrangler: + optional: true + + '@react-router/express@7.0.2': + resolution: {integrity: sha512-rhKt/bylEdZNHKzOI8NzP6b27fiJ2zjf59b/boWWOwjuDk6ZEdV1iLa2Nvm55qN1mj2zSx3H/9Tx/ZPHgrGEgw==} + engines: {node: '>=20.0.0'} + peerDependencies: + express: ^4.17.1 + react-router: 7.0.2 + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@react-router/node@7.0.2': + resolution: {integrity: sha512-6Of5M2wP9QgYlR+boR0ptPjh3UyfaNvPMKQihowTGjAjUZIoNqz4iBn8ClNsLFbT3KQewcnNTHi2p+Ou7S4ZyQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react-router: 7.0.2 + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@react-router/serve@7.0.2': + resolution: {integrity: sha512-kVoU2aeCRJ7rMWXtzJaFw52DgGhk8SqbgGpVQXIOj2QFN2rqrDz7L/WyIhCeOEztTSBgZxID0qDE7DPdbnHkkQ==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + react-router: 7.0.2 + '@react-stately/utils@3.10.4': resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==} peerDependencies: @@ -3298,6 +3398,9 @@ packages: '@types/node@14.18.63': resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + '@types/node@20.17.9': + resolution: {integrity: sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==} + '@types/node@22.9.0': resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} @@ -3767,6 +3870,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + babel-dead-code-elimination@1.0.6: + resolution: {integrity: sha512-JxFi9qyRJpN0LjEbbjbN8g0ux71Qppn9R8Qe3k6QzHg2CaKsbUQtbn307LQGiDLGjV6JCtEFqfxzVig9MyDCHQ==} + babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110: resolution: {integrity: sha512-WdxXtLxsV4gh/GlEK4fuFDGkcED0Wb9UJEBB6Uc1SFqRFEmJNFKboW+Z4NUS5gYrPImqrjh4IwHAmgS6ZBg4Cg==} @@ -4180,6 +4286,10 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -7486,6 +7596,26 @@ packages: peerDependencies: react: '>=16.8' + react-router@7.0.1: + resolution: {integrity: sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-router@7.0.2: + resolution: {integrity: sha512-m5AcPfTRUcjwmhBzOJGEl6Y7+Crqyju0+TgTQxoS4SO+BkWbhOrcfZNq6wSWdl2BBbJbsAoBUb8ZacOFT+/JlA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react-smooth@4.0.1: resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==} peerDependencies: @@ -10064,6 +10194,8 @@ snapshots: '@microsoft/tsdoc@0.15.0': {} + '@mjackson/node-fetch-server@0.2.0': {} + '@next/env@15.0.3': {} '@next/swc-darwin-arm64@15.0.3': @@ -11008,6 +11140,87 @@ snapshots: clsx: 2.1.1 react: 19.0.0 + '@react-router/dev@7.0.2(@react-router/serve@7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3))(@types/node@20.17.9)(lightningcss@1.27.0)(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(terser@5.34.1)(typescript@5.6.3)(vite@5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1))': + dependencies: + '@babel/core': 7.25.7 + '@babel/generator': 7.25.7 + '@babel/parser': 7.25.7 + '@babel/plugin-syntax-decorators': 7.25.7(@babel/core@7.25.7) + '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.7) + '@babel/preset-typescript': 7.25.7(@babel/core@7.25.7) + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.7 + '@npmcli/package-json': 4.0.1 + '@react-router/node': 7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3) + arg: 5.0.2 + babel-dead-code-elimination: 1.0.6 + chokidar: 4.0.1 + dedent: 1.5.3 + es-module-lexer: 1.5.4 + exit-hook: 2.2.1 + fs-extra: 10.1.0 + gunzip-maybe: 1.4.2 + jsesc: 3.0.2 + lodash: 4.17.21 + pathe: 1.1.2 + picocolors: 1.1.1 + picomatch: 2.3.1 + prettier: 2.8.8 + react-refresh: 0.14.2 + react-router: 7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + semver: 7.6.3 + set-cookie-parser: 2.7.0 + valibot: 0.41.0(typescript@5.6.3) + vite: 5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1) + vite-node: 1.6.0(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1) + optionalDependencies: + '@react-router/serve': 7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - bluebird + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + '@react-router/express@7.0.2(express@4.21.1)(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3)': + dependencies: + '@react-router/node': 7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3) + express: 4.21.1 + react-router: 7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + optionalDependencies: + typescript: 5.6.3 + + '@react-router/node@7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3)': + dependencies: + '@mjackson/node-fetch-server': 0.2.0 + react-router: 7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + source-map-support: 0.5.21 + stream-slice: 0.1.2 + undici: 6.20.0 + optionalDependencies: + typescript: 5.6.3 + + '@react-router/serve@7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3)': + dependencies: + '@react-router/express': 7.0.2(express@4.21.1)(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3) + '@react-router/node': 7.0.2(react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.6.3) + compression: 1.7.4 + express: 4.21.1 + get-port: 5.1.1 + morgan: 1.10.0 + react-router: 7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + source-map-support: 0.5.21 + transitivePeerDependencies: + - supports-color + - typescript + '@react-stately/utils@3.10.4(react@19.0.0)': dependencies: '@swc/helpers': 0.5.13 @@ -11904,6 +12117,10 @@ snapshots: '@types/node@14.18.63': {} + '@types/node@20.17.9': + dependencies: + undici-types: 6.19.8 + '@types/node@22.9.0': dependencies: undici-types: 6.19.8 @@ -12505,6 +12722,15 @@ snapshots: axobject-query@4.1.0: {} + babel-dead-code-elimination@1.0.6: + dependencies: + '@babel/core': 7.25.7 + '@babel/parser': 7.25.7 + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.7 + transitivePeerDependencies: + - supports-color + babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110: dependencies: '@babel/types': 7.25.7 @@ -12931,6 +13157,8 @@ snapshots: cookie@0.7.1: {} + cookie@1.0.2: {} + core-util-is@1.0.2: {} core-util-is@1.0.3: {} @@ -16886,6 +17114,26 @@ snapshots: '@remix-run/router': 1.21.0 react: 19.0.0 + react-router@7.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@types/cookie': 0.6.0 + cookie: 1.0.2 + react: 19.0.0 + set-cookie-parser: 2.7.0 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 19.0.0(react@19.0.0) + + react-router@7.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@types/cookie': 0.6.0 + cookie: 1.0.2 + react: 19.0.0 + set-cookie-parser: 2.7.0 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 19.0.0(react@19.0.0) + react-smooth@4.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: fast-equals: 5.0.1 @@ -18393,6 +18641,24 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@1.6.0(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1): + dependencies: + cac: 6.7.14 + debug: 4.3.7(supports-color@8.1.1) + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@1.6.0(@types/node@22.9.0)(lightningcss@1.27.0)(terser@5.34.1): dependencies: cac: 6.7.14 @@ -18429,6 +18695,17 @@ snapshots: - supports-color - terser + vite-tsconfig-paths@5.1.2(typescript@5.6.3)(vite@5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1)): + dependencies: + debug: 4.3.7(supports-color@8.1.1) + globrex: 0.1.2 + tsconfck: 3.1.3(typescript@5.6.3) + optionalDependencies: + vite: 5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1) + transitivePeerDependencies: + - supports-color + - typescript + vite-tsconfig-paths@5.1.2(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(lightningcss@1.27.0)(terser@5.34.1)): dependencies: debug: 4.3.7(supports-color@8.1.1) @@ -18440,6 +18717,17 @@ snapshots: - supports-color - typescript + vite@5.4.11(@types/node@20.17.9)(lightningcss@1.27.0)(terser@5.34.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.24.0 + optionalDependencies: + '@types/node': 20.17.9 + fsevents: 2.3.3 + lightningcss: 1.27.0 + terser: 5.34.1 + vite@5.4.11(@types/node@22.9.0)(lightningcss@1.27.0)(terser@5.34.1): dependencies: esbuild: 0.21.5 diff --git a/turbo.json b/turbo.json index 129c8bad9..00d74effc 100644 --- a/turbo.json +++ b/turbo.json @@ -21,7 +21,12 @@ "dependsOn": ["^build"], "env": ["REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN"] }, - "e2e-react-router#build": { + "e2e-react-router-v6#build": { + "outputs": ["dist/**", "cypress/**"], + "dependsOn": ["^build"], + "env": ["REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN"] + }, + "e2e-react-router-v7#build": { "outputs": ["dist/**", "cypress/**"], "dependsOn": ["^build"], "env": ["REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN"] @@ -58,7 +63,11 @@ "dependsOn": ["build"], "env": ["REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN"] }, - "e2e-react-router#test": { + "e2e-react-router-v6#test": { + "dependsOn": ["build"], + "env": ["REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN"] + }, + "e2e-react-router-v7#test": { "dependsOn": ["build"], "env": ["REACT_COMPILER", "E2E_NO_CACHE_ON_RERUN"] },