├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── babel.config.cjs ├── codecov.yml ├── eslint.config.js ├── examples ├── app │ ├── .gitignore │ ├── @types │ │ └── nextjs-routes.d.ts │ ├── README.md │ ├── eslint.config.cjs │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── src │ │ └── app │ │ │ ├── [store] │ │ │ ├── client.tsx │ │ │ └── page.tsx │ │ │ ├── client.tsx │ │ │ ├── head.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ └── tsconfig.json ├── cjs │ ├── .gitignore │ ├── README.md │ ├── __test__ │ │ └── index.test.tsx │ ├── eslint.config.cjs │ ├── jest.config.mjs │ ├── jest.setup.js │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── bars │ │ │ └── [bar].tsx │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── tsconfig.json │ └── types │ │ └── nextjs-routes.d.ts ├── intl │ ├── .gitignore │ ├── @types │ │ └── nextjs-routes.d.ts │ ├── README.md │ ├── eslint.config.cjs │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── src │ │ └── pages │ │ │ ├── [store].tsx │ │ │ ├── _app.tsx │ │ │ └── index.tsx │ └── tsconfig.json └── typescript │ ├── .gitignore │ ├── README.md │ ├── __test__ │ └── index.test.tsx │ ├── eslint.config.cjs │ ├── jest.config.mjs │ ├── jest.setup.js │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── api │ │ └── hello.ts │ ├── bars │ │ └── [bar].tsx │ ├── foos │ │ └── [foo].tsx │ └── index.tsx │ ├── public │ ├── favicon.ico │ └── vercel.svg │ ├── tsconfig.json │ └── types │ └── nextjs-routes.d.ts ├── images └── nextjs-routes.gif ├── jest.config.cjs ├── package.json ├── packages ├── e2e │ ├── @types │ │ └── nextjs-routes.d.ts │ ├── app │ │ ├── bars │ │ │ └── [bar] │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── e2e.test.ts │ ├── eslint.config.cjs │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── [...slug].tsx │ │ ├── foos │ │ │ └── [foo].tsx │ │ └── index.tsx │ ├── tsconfig.json │ └── typetest.tsx └── nextjs-routes │ ├── .npmignore │ ├── package.json │ ├── src │ ├── cli.ts │ ├── config.ts │ ├── core.ts │ ├── index.ts │ ├── logger.ts │ ├── test │ │ ├── __snapshots__ │ │ │ └── core.test.ts.snap │ │ ├── core.test.ts │ │ ├── index.test.ts │ │ ├── logger.test.ts │ │ └── utils.test.ts │ ├── utils.ts │ └── version.ts │ ├── tsconfig.json │ └── webpack.config.cjs ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "bug:" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A description of what the bug is. 11 | 12 | **Context (please complete the following information):** 13 | 14 | - nextjs-routes version: 15 | - Are you using the [pages](https://nextjs.org/docs/pages/building-your-application/routing) directory, [app](https://nextjs.org/docs/app/building-your-application/routing) directory, or both? 16 | - Are you using nextjs-routes via [next.config.js](https://github.com/tatethurston/nextjs-routes?tab=readme-ov-file#installation--usage-) or the CLI (`npx nextjs-routes`)? 17 | - nextjs version: 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "feature:" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | build-package: 9 | name: Build Package 10 | runs-on: ubuntu-latest 11 | outputs: 12 | package: ${{ steps.publish-local-package.outputs.package }} 13 | steps: 14 | - uses: tatethurston/github-actions/publish-local-package@main 15 | id: publish-local-package 16 | with: 17 | path: packages/nextjs-routes 18 | ci: 19 | name: Lint and Test 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: tatethurston/github-actions/test@main 23 | with: 24 | codecov_token: ${{ secrets.CODECOV_TOKEN }} 25 | - uses: tatethurston/github-actions/check-generated-files@main 26 | with: 27 | cmd: pnpm examples:regen 28 | ci-windows: 29 | name: Windows CI 30 | runs-on: windows-latest 31 | needs: build-package 32 | steps: 33 | - uses: tatethurston/github-actions/install-local-package@main 34 | with: 35 | name: ${{ needs.build-package.outputs.package }} 36 | - name: Bin Check 37 | run: | 38 | echo 'module.exports = {}' > next.config.js 39 | mkdir pages 40 | npx --no nextjs-routes 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM Package 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | jobs: 7 | publish: 8 | name: Publish to NPM 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: tatethurston/github-actions/publish@main 12 | with: 13 | package_directory: packages/nextjs-routes 14 | npm_token: ${{ secrets.NPM_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | node_modules 4 | tsconfig.tsbuildinfo 5 | .next 6 | .vscode 7 | # copied over durring build 8 | packages/nextjs-routes/LICENSE 9 | packages/nextjs-routes/README.md 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # pnpm run e2e:setup 2 | # pnpm run examples:regen 3 | pnpm run lint:fix 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | public.package.json 4 | .next 5 | .swc 6 | out 7 | node_modules 8 | pnpm-lock.yaml 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.2.5 4 | 5 | - Fix CLI invocation on Windows. 6 | 7 | ## 2.2.4 8 | 9 | - CLI invocation now reads next.config.js or next.config.mjs. 10 | - Fix `route`'s handling of query keys whose value is `undefined`. Fixes #206. Thanks @sleepdotexe! 11 | 12 | ## 2.2.3 13 | 14 | - Bug fix: `usePathname` and `useParams` were incorrectly resolving to `any` return types. 15 | 16 | ## 2.2.2 17 | 18 | - Adds support for Next.js's `app` directory. `Link` accepts either static routes (no url parameters) or a `RouteLiteral` string, which can be generated by the `route` helper from this library: 19 | 20 | ```tsx 21 | import { route } from "nextjs-routes"; 22 | 23 | 29 | Baz 30 | ; 31 | ``` 32 | 33 | - Add `RouteLiteral` type. This type represents a string that has been confirmed to be a validated application route and can be passed to `Link` or `useRouter`. This is a TypeScript branded type. 34 | 35 | ```ts 36 | import { RouteLiteral } from "nextjs-routes"; 37 | ``` 38 | 39 | `route` returns a `RouteLiteral`. If you construct a route string you can cast it to a `RouteLiteral` so that `Link` and `useRouter` will accept it: 40 | 41 | ``` 42 | const myRoute = `/foos/${foo}` as RouteLiteral 43 | ``` 44 | 45 | In general, prefer using the `route` helper to generate routes. 46 | 47 | - Refine types for `usePathname`, `useRouter` and `useParams` from `"next/navigation"` to use `nextjs-routes` generated types. 48 | 49 | - Fix generated routes when using [parallel-routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) and [intercepting-routes](https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes). 50 | 51 | - Fix `ref` type for `Link`. Previously `ref` was missing, now it's correctly typed. 52 | 53 | ## 2.2.1 54 | 55 | - Fix route generation on Windows. See [#187](https://github.com/tatethurston/nextjs-routes/issues/187). Thanks @AkanoCA! 56 | 57 | ## 2.2.0 58 | 59 | - Add `trailingSlash` option to `route`. See [#168](https://github.com/tatethurston/nextjs-routes/issues/168) 60 | - Fix the generated optional catch all route type. See [#183](https://github.com/tatethurston/nextjs-routes/issues/183) 61 | - Sort the generated route types lexicographically. 62 | 63 | ## 2.1.0 64 | 65 | - Add experimental support for app directory [route handler](https://nextjs.org/docs/app/building-your-application/routing/route-handlers). See [#178](https://github.com/tatethurston/nextjs-routes/issues/178). 66 | 67 | ## 2.0.1 68 | 69 | - Fix `GetServerSidePropsContext` and `GetServerSidePropsResult` types. Thanks @po4tion! 70 | - Change generated file location from `nextjs-routes.d.ts` to `@types/nextjs-routes.d.ts`. Thanks @po4tion! 71 | 72 | To preserve the old location, make the following change to your `next.config.js`: 73 | 74 | ```diff 75 | const nextRoutes = require("nextjs-routes/config"); 76 | const withRoutes = nextRoutes({ 77 | + outDir: "", 78 | }); 79 | ``` 80 | 81 | Otherwise, delete your old `nextjs-routes.d.ts` once `@types/nextjs-routes.d.ts` is generated. 82 | 83 | ## 2.0.0 84 | 85 | - `router.query` types must now be narrowed using `router.isReady`. This ensures types are correct for pages that use [Automatic Static Optimization](https://nextjs.org/docs/advanced-features/automatic-static-optimization). 86 | 87 | [Next's documentation](https://nextjs.org/docs/advanced-features/automatic-static-optimization) notes the following: 88 | 89 | > During prerendering, the router's query object will be empty since we do not have query information to provide during this phase. After hydration, Next.js will trigger an update to your application to provide the route parameters in the query object. 90 | 91 | See [#117](https://github.com/tatethurston/nextjs-routes/issues/117) for more context and discussion. 92 | 93 | - Route type generation can now also be generated outside of `next build` / `next dev` by using `npx nextjs-routes`. 94 | 95 | ## 1.0.9 96 | 97 | - Enable `Link` and `router` methods to only update the route `hash`. Thanks @sitch! 98 | - Publish commonjs for `route` runtime so jest tests don't require transpilation for users. Thanks @panudetjt! 99 | - Add `GetServerSidePropsContext` type. Thanks @slhck! 100 | 101 | ## 1.0.8 102 | 103 | - Fix type definition import path for `nextjs-routes/config` so it is visible to `tsc`. 104 | 105 | ## 1.0.7 106 | 107 | - Remove package.json version import. This resolves `TypeError[ERR_IMPORT_ASSERTION_TYPE_MISSING]`. [See #115](https://github.com/tatethurston/nextjs-routes/issues/115) for more context. 108 | 109 | ## 1.0.6 110 | 111 | - Fix bad publish of 1.0.5 112 | 113 | ## 1.0.5 114 | 115 | - The version of `nextjs-routes` is now included in the generated `nextjs-routes.d.ts` file. 116 | - Switch `Link` to use TypeScript unions instead of function overloading. Function overloading resulted in errors that were difficult for users to understand, and [created issues for some use cases](https://github.com/tatethurston/nextjs-routes/issues/111). 117 | 118 | ## 1.0.4 119 | 120 | - `LinkProps` now accept path strings for static routes: 121 | 122 | ```tsx 123 | import { LinkProps } from "next/link"; 124 | // previously this would error 125 | const props: LinkProps = { href: "/foo" }; 126 | ``` 127 | 128 | ## 1.0.3 129 | 130 | - The `Route` type now includes `hash`. This enables the following: 131 | 132 | ```tsx 133 | // this will link to /foo#bar 134 | Foo 135 | ``` 136 | 137 | ## 1.0.2 138 | 139 | - `Link` now [accepts anchor props](https://beta.nextjs.org/docs/api-reference/components/link): 140 | 141 | ```tsx 142 | 143 | Dashboard 144 | 145 | ``` 146 | 147 | ## 1.0.1 148 | 149 | - Update `NextRouter` type to keep `query` and `pathname` bound in a union. This allows you to use `router` from `useRouter` as an argument to `router.push` or `router.replace`: 150 | 151 | ```ts 152 | const router = useRouter(); 153 | // reload the current page, preserving search parameters 154 | router.push(router, undefined, { locale: "fr" }); 155 | ``` 156 | 157 | ## 1.0.0 158 | 159 | - This library will now follow [semantic versioning](https://docs.npmjs.com/about-semantic-versioning). 160 | 161 | - The previously deprecated direct invocation of nextjs-routes via `npx nextjs-routes` has been removed in favor of automatic regeneration via [withRoutes](https://github.com/tatethurston/nextjs-routes#installation--usage-). See [#63](https://github.com/tatethurston/nextjs-routes/issues/63) for the motivation behind this change or to voice any concerns. 162 | 163 | ## 0.1.7 164 | 165 | - Support [Next 13 app (beta) directory](https://nextjs.org/docs/advanced-features/custom-app) 166 | 167 | - Add `dir` option to support non standard NextJS project structures such as [Nx](https://nx.dev/packages/next): 168 | 169 | ```js 170 | // next.config.js 171 | const withRoutes = require("nextjs-routes/config")({ dir: __dirname }); 172 | ``` 173 | 174 | Thanks [@alexgorbatchev](https://github.com/alexgorbatchev) for the contribution! 175 | 176 | ## 0.1.6 177 | 178 | - Accept path strings for static routes: 179 | 180 | ```tsx 181 | Foo 182 | ``` 183 | 184 | Thanks [@MariaSolOs](https://github.com/MariaSolOs) for the contribution! 185 | 186 | - Use function overloads for `Link` and `router.push` and `router.replace`. This yields better hints for typos in pathnames: 187 | 188 | ```tsx 189 | Foo 190 | ``` 191 | 192 | Previously: 193 | `[tsserver 2322] [E] Type '"/foos/[foo]"' is not assignable to type '"/"'.` 194 | 195 | Now: 196 | `Type '"/foosy/[foo]"' is not assignable to type '"/api/hello" | "/bars/[bar]" | "/foos/[foo]" | "/"'. Did you mean '"/foos/[foo]"'?` (+2 other overload errors). 197 | 198 | ## 0.1.5 199 | 200 | - Export `Locale` from `nextjs-routes`. 201 | 202 | ```ts 203 | import { Locale } from "nextjs-routes"; 204 | ``` 205 | 206 | Thanks [@Asamsig](https://github.com/Asamsig) for the contribution! 207 | 208 | ## 0.1.4 209 | 210 | - `nextjs-routes` now generates route types for [Nextjs i18n configuration](https://nextjs.org/docs/advanced-features/i18n-routing). Eg the following next config: 211 | 212 | ```js 213 | module.exports = withRoutes({ 214 | i18n: { 215 | defaultLocale: "de-DE", 216 | locales: ["de-DE", "en-FR", "en-US"], 217 | }, 218 | }); 219 | ``` 220 | 221 | Will make `locale` typed as `'de-DE' | 'en-FR' | 'en-US'` for `Link` and `useRouter`. 222 | 223 | ## 0.1.3 224 | 225 | - `nextjs-routes` [pageExtensions](https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions) has been updated to respect multiple extensions such as `.page.tsx`. In `0.1.2`, only single extensions `.tsx` were respected. This is now identical behavior to Next.js. 226 | 227 | ## 0.1.2 228 | 229 | - `nextjs-routes` now respects [pageExtensions](https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions) from `next.config.js`. 230 | 231 | ## 0.1.1 232 | 233 | [ skipped ] 234 | 235 | ## 0.1.0 236 | 237 | This release contains a breaking change, indicated by the minor version bump to 0.1.0. `nextjs-routes` has not yet reached v1, but will follow semantic versioning once it does. Until then, minor version changes will be used to help flag breaking changes. 238 | 239 | - Breaking change: the `withRoutes` import path and invocation has changed to better align with the general pattern in the Nextjs plugin ecosystem and to support configuration options, notably the new `outDir` option. It also now includes an ESM export to support usage in `next.config.mjs`. 240 | 241 | ```diff 242 | - const { withRoutes } = require("nextjs-routes/next-config.cjs"); 243 | + const withRoutes = require("nextjs-routes/config")(); 244 | 245 | /** @type {import('next').NextConfig} */ 246 | const nextConfig = { 247 | reactStrictMode: true, 248 | }; 249 | 250 | module.exports = withRoutes(nextConfig); 251 | ``` 252 | 253 | Note the import path has changed and the import itself has changed to function that is invoked with any configuration options. This provides better ergonomics for configuration options: 254 | 255 | ```js 256 | const withRoutes = require("nextjs-routes/config")({ outDir: "types" }); 257 | ``` 258 | 259 | - The type `RoutedQuery` has been added to retrieve the `Query` for a given `Route`. This is useful as the context type parameter inside `getStaticProps` and `getServerSideProps`. Thanks [@MariaSolOs](https://github.com/MariaSolOs) for the contribution! 260 | 261 | - `withRoutes` now accepts `outDir` as a configuration option to dictate where `nextjs-routes.d.ts` is generated. Thanks [@MariaSolOs](https://github.com/MariaSolOs) for the contribution! 262 | 263 | ## 0.0.22 264 | 265 | - Deprecate direct invocation of nextjs-routes in favor of automatic regeneration via [withRoutes](https://github.com/tatethurston/nextjs-routes#installation--usage-). See [#63](https://github.com/tatethurston/nextjs-routes/issues/63) for the motivation behind this change or to voice any concerns. 266 | 267 | ## 0.0.21 268 | 269 | - Add `route` runtime for generating type safe pathnames from a `Route` object 270 | 271 | This can be used to fetch from API routes: 272 | 273 | ```ts 274 | import { route } from "nextjs-routes"; 275 | 276 | fetch(route({ pathname: "/api/foos/[foo]", query: { foo: "foobar" } })); 277 | ``` 278 | 279 | Or for type safe redirects from `getServerSideProps`: 280 | 281 | ```ts 282 | import { route } from "nextjs-routes"; 283 | 284 | export const getServerSideProps: GetServerSideProps = async (context) => { 285 | return { 286 | redirect: { 287 | destination: route({ pathname: "/foos/[foo]", query: { foo: "foobar" } }), 288 | permanent: false, 289 | }, 290 | }; 291 | }; 292 | ``` 293 | 294 | ## 0.0.20 295 | 296 | - Move `chokidar` from `devDependencies` to `dependencies` so it's installed automatically. 297 | 298 | ## 0.0.19 299 | 300 | - Bug Fix: quote query segments in generated types. See [#49](https://github.com/tatethurston/nextjs-routes/issues/49) for more context. 301 | - Bug Fix: don't generate routes for non navigable routes (`_error`, `_app`, `_document`). 302 | - Bug Fix: don't generate routes for test files that are co-located in pages directory. See [#50](https://github.com/tatethurston/nextjs-routes/pull/50) for more context. 303 | 304 | ## 0.0.18 305 | 306 | - `query` is now typed as `string | string[] | undefined` instead of `string | undefined`. 307 | - `nextjs-routes` can now be configured via your `next.config.js` to automatically regenerate types whenever your routes change: 308 | 309 | ```js 310 | // next.config.js 311 | 312 | /** @type {import('next').NextConfig} */ 313 | const { withRoutes } = require("nextjs-routes/next-config.cjs"); 314 | 315 | const nextConfig = { 316 | reactStrictMode: true, 317 | }; 318 | 319 | module.exports = withRoutes(nextConfig); 320 | ``` 321 | 322 | This wiring will only run in Next.js' development server (eg `npx next dev`) and `withRoutes` will no-op in production. 323 | 324 | ## 0.0.17 325 | 326 | - re-export types from `next/link` and `next/router`. 327 | - remove prettier as a peer dependency. 328 | - enable src/pages for windows users. 329 | - routes are now generated for routes that start with `_`. `_app`, `_document`, `_error` and `middleware` are excluded. 330 | - gracefully handles missing pages directory and no pages. 331 | 332 | ## 0.0.16 333 | 334 | - fixed prettier as an optional peer dependency 335 | 336 | ## 0.0.15 337 | 338 | - nextjs-routes no longer adds types to the global type namespace. Previously, 339 | `Route` was available globally. Now, it must be imported: 340 | 341 | ```ts 342 | import type { Route } from "nextjs-routes"; 343 | ``` 344 | 345 | - query from `useRouter` is now correctly typed as `string | undefined` instead of `string`. If you know the current route, you can supply a type argument to narrow required parameters to `string`, eg: 346 | 347 | ``` 348 | // if you have a page /foos/[foo].ts 349 | 350 | const router = useRouter<"/foos/[foo]">(); 351 | // foo will be typed as a string, because the foo query parameter is required and thus will always be present. 352 | const { foo } = router.query; 353 | ``` 354 | 355 | ## 0.0.14 356 | 357 | - Allow passing in `query` without `pathname` to change current url parameters. 358 | - `router.query` can no longer be `undefined`. 359 | 360 | ## 0.0.13 361 | 362 | - Support search parameters. See [#17](https://github.com/tatethurston/nextjs-routes/issues/17) for more context. 363 | 364 | ## 0.0.12 365 | 366 | - Removed reexports of `next/link` and `next/router`. 367 | 368 | This means replacing imports of `next/link` with `nextjs-routes/link` and `next/router` with `nextjs-routes/router` is no longer necessary: 369 | 370 | ```diff 371 | -import Link from "nextjs-routes/link"; 372 | +import Link from "next/link"; 373 | ``` 374 | 375 | ```diff 376 | -import { useRouter } from 'nextjs-routes/router' 377 | +import { useRouter } from 'next/router' 378 | ``` 379 | 380 | - Added windows support. 381 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 👫 2 | 3 | Thanks for helping make this project better! 4 | 5 | ## Report an Issue 🐛 6 | 7 | If you find a bug or want to discuss a new feature, please [create a new issue](https://github.com/tatethurston/nextjs-routes/issues). If you'd prefer to keep things private, feel free to [email me](mailto:tatethurston@gmail.com?subject=nextjs-routes). 8 | 9 | ## Contributing Code with Pull Requests 🎁 10 | 11 | Please create a [pull request](https://github.com/tatethurston/nextjs-routes/pulls). Expect a few iterations and some discussion before your pull request is merged. If you want to take things in a new direction, feel free to fork and iterate without hindrance! 12 | 13 | ## Code of Conduct 🧐 14 | 15 | My expectations for myself and others is to strive to build a diverse, inclusive, safe community. 16 | 17 | For more guidance, check out [thoughtbot's code of conduct](https://thoughtbot.com/open-source-code-of-conduct). 18 | 19 | ## Licensing 📃 20 | 21 | See the project's [MIT License](https://github.com/tatethurston/nextjs-routes/blob/main/LICENSE). 22 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Local Development 2 | 3 | ### First time setup 4 | 5 | From the project directory root: 6 | 7 | 1. `nvm use` 8 | 1. `pnpm install` 9 | 1. `pnpm package:build` 10 | 11 | The source code for the package is in `packages/nextjs-routes/`. 12 | 13 | There are examples that use the locally built package in `examples/`. 14 | 15 | There is an e2e test that runs against a minimal nextjs application in `packages/e2e/`. 16 | 17 | ### Testing 18 | 19 | Tests are run with jest. 20 | 21 | From the project directory root: 22 | 23 | `pnpm test` 24 | 25 | ### Linting 26 | 27 | As part of installation, husky pre-commit hooks are installed to run linters against the repo. 28 | 29 | ### Publishing 30 | 31 | There are CI and publishing GitHub workflows in `./github/workflows`. These are named `ci.yml` and `publish.yml`. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tate Thurston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Routes 2 | 3 |
Type safe routing for Next.js
4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | ![nextjs-routes preview gif](./images/nextjs-routes.gif) 26 | 27 | ## What is this? 🧐 28 | 29 | `nextjs-routes` generates type safe routing utilities from your `pages` and/or `app` directory. 30 | 31 | ## Notice 32 | 33 | If you are using Next.js's App Router you may not need this library. Next.js provides an [experimental option to generate typed links](https://nextjs.org/docs/app/building-your-application/configuring/typescript#statically-typed-links). Next.js's option only works for the `app` directory, and not `pages`. If you're using the `pages` directory, or if you're using the `app` directory and want to use typed objects instead of string interpolation to provide URL parameters and queries, use this library. 34 | 35 | ## Highlights 36 | 37 | 🦄 Zero config 38 | 39 | 💨 Types only -- zero runtime (pages directory only) 40 | 41 | 🛠 No more broken links 42 | 43 | 🪄 Route autocompletion 44 | 45 | 🔗 Supports all Next.js route types: static, dynamic, catch all and optional catch all 46 | 47 | ## Installation & Usage 📦 48 | 49 | 1. Add this package to your project: 50 | 51 | ```sh 52 | npm install nextjs-routes 53 | # or 54 | yarn add nextjs-routes 55 | # or 56 | pnpm add nextjs-routes 57 | ``` 58 | 59 | 2. Update your `next.config.js`: 60 | 61 | ```diff 62 | + const nextRoutes = require("nextjs-routes/config"); 63 | + const withRoutes = nextRoutes(); 64 | 65 | /** @type {import('next').NextConfig} */ 66 | const nextConfig = { 67 | reactStrictMode: true, 68 | }; 69 | 70 | - module.exports = nextConfig; 71 | + module.exports = withRoutes(nextConfig); 72 | ``` 73 | 74 | 3. Start or build your next project: 75 | 76 | ```sh 77 | npx next dev 78 | # or 79 | npx next build 80 | ``` 81 | 82 | That's it! A `@types/nextjs-routes.d.ts` file will be generated the first time you start your server. Check this file into version control. `next/link` and `next/router` type definitions have been augmented to verify your application's routes. No more broken links, and you get route autocompletion 🙌. 83 | 84 | In development, whenever your routes change, your `@types/nextjs-routes.d.ts` file will automatically update. 85 | 86 | If you would prefer to generate the route types file outside of `next dev` or `next build` you can also invoke the cli directly: `npx nextjs-routes`. 87 | 88 | ## Examples 🛠 89 | 90 | ### Link 91 | 92 | `Link`'s `href` prop is now typed based on your application routes: 93 | 94 | ```tsx 95 | import Link from "next/link"; 96 | 97 | 103 | Bar 104 | ; 105 | ``` 106 | 107 | If the route doesn't require any parameters, you can also use a path string: 108 | 109 | ```tsx 110 | Foo 111 | ``` 112 | 113 | ### useRouter 114 | 115 | `useRouter`'s returned router instance types for `push`, `replace` and `query` are now typed based on your application routes. 116 | 117 | Identical to `Link`, `push` and `replace` now expect a UrlObject or path string: 118 | 119 | #### push 120 | 121 | ```tsx 122 | import { useRouter } from "next/router"; 123 | 124 | const router = useRouter(); 125 | router.push({ pathname: "/foos/[foo]", query: { foo: "test" } }); 126 | ``` 127 | 128 | #### replace 129 | 130 | ```tsx 131 | import { useRouter } from "next/router"; 132 | 133 | const router = useRouter(); 134 | router.replace({ pathname: "/" }); 135 | ``` 136 | 137 | #### query 138 | 139 | ```tsx 140 | import { useRouter } from "next/router"; 141 | 142 | // query is typed as a union of all query parameters defined by your application's routes 143 | const { query } = useRouter(); 144 | ``` 145 | 146 | By default, `query` will be typed as the union of all possible query parameters defined by your application routes. If you'd like to narrow the type to fewer routes or a single page, you can supply a type argument: 147 | 148 | ```tsx 149 | import { useRouter } from "next/router"; 150 | 151 | const router = useRouter<"/foos/[foo]">(); 152 | // query is now typed as `{ foo?: string | undefined }` 153 | router.query; 154 | ``` 155 | 156 | You can further narrow the query type by checking the router's `isReady` property. 157 | 158 | ```tsx 159 | import { useRouter } from "next/router"; 160 | 161 | const router = useRouter<"/foos/[foo]">(); 162 | // query is typed as `{ foo?: string | undefined }` 163 | router.query; 164 | 165 | if (router.isReady) { 166 | // query is typed as `{ foo: string }` 167 | router.query; 168 | } 169 | ``` 170 | 171 | Checking `isReady` is necessary because of Next's [Automatic Static Optimization](https://nextjs.org/docs/advanced-features/automatic-static-optimization). The router's query object will be empty for pages that are Automatic Static Optimized. After hydration, Next.js will trigger an update to your application to provide the route parameters in the query object. See [Next's documentation](https://nextjs.org/docs/advanced-features/automatic-static-optimization) for more information. `isReady` will always return true for server rendered pages. 172 | 173 | ### Route 174 | 175 | If you want to use the generated `Route` type in your code, you can import it from `nextjs-routes`: 176 | 177 | ```ts 178 | import type { Route } from "nextjs-routes"; 179 | ``` 180 | 181 | ### Pathname 182 | 183 | If you want a type for all possible `pathname`s you can achieve this via `Route`: 184 | 185 | ```ts 186 | import type { Route } from "nextjs-routes"; 187 | 188 | // '/' | '/foos/[foo]' | 'other-route' | ... 189 | type Pathname = Route["pathname"]; 190 | ``` 191 | 192 | ### RoutedQuery 193 | 194 | If you want to use the generated `Query` for a given `Route`, you can import it from `nextjs-routes`: 195 | 196 | ```ts 197 | // Query | Query & { foo: string } | ... 198 | import type { RoutedQuery } from "nextjs-routes"; 199 | ``` 200 | 201 | By default, `query` will be typed as the union of all possible query parameters defined by your application routes. If you'd like to narrow the type to fewer routes or a single page, you can supply the path as a type argument: 202 | 203 | ```ts 204 | // Query & { foo: string } 205 | type FooRouteQuery = RoutedQuery<"/foos/[foo]">; 206 | ``` 207 | 208 | ### GetServerSidePropsContext 209 | 210 | If you're using `getServerSideProps` consider using `GetServerSidePropsContext` from nextjs-routes. This is nearly identical to `GetServerSidePropsContext` from next, but further narrows types based on nextjs-route's route data. 211 | 212 | ```ts 213 | import type { GetServerSidePropsContext } from "nextjs-routes"; 214 | 215 | export function getServerSideProps( 216 | context: GetServerSidePropsContext<"/foos/[foo]">, 217 | ) { 218 | // context.params will include `foo` as a string; 219 | const { foo } = context.params; 220 | } 221 | ``` 222 | 223 | ### GetServerSideProps 224 | 225 | If you're using `getServerSideProps` and TypeScript 4.9 or later, you can combine the [satisfies](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator) operator with `GetServerSideProps` from nextjs-routes. This is nearly identical to `GetServerSideProps` from next, but further narrows types based on nextjs-route's route data. 226 | 227 | ```ts 228 | import type { GetServerSideProps } from "nextjs-routes"; 229 | 230 | export const getServerSideProps = (async (context) => { 231 | // context.params will include `foo` as a string; 232 | const { foo } = context.params; 233 | }) satisfies GetServerSideProps<{}, "/foos/[foo]">; 234 | ``` 235 | 236 | ## How does this work? 🤔 237 | 238 | `nextjs-routes` generates types for the `pathname` and `query` for every page in your `pages` directory. The generated types are written to `@types/nextjs-routes.d.ts` which is automatically referenced by your Next project's `tsconfig.json`. `@types/nextjs-routes.d.ts` redefines the types for `next/link` and `next/router` and applies the generated route types. 239 | 240 | ## What if I need a runtime? 241 | 242 | There are some cases where you may want to generate a type safe path from a `Route` object, such as when `fetch`ing from an API route or serving redirects from `getServerSideProps`. These accept `strings` instead of the `Route` object that `Link` and `useRouter` accept. Because these do not perform the same string interpolation for dynamic routes, runtime code is required instead of a type only solution. 243 | 244 | For these cases, you can use `route` from `nextjs-routes`: 245 | 246 | ### fetch 247 | 248 | ```ts 249 | import { route } from "nextjs-routes"; 250 | 251 | fetch(route({ pathname: "/api/foos/[foo]", query: { foo: "foobar" } })); 252 | ``` 253 | 254 | ### getServerSideProps 255 | 256 | ```ts 257 | import { route, type GetServerSidePropsContext } from "nextjs-routes"; 258 | 259 | export function getServerSideProps(context: GetServerSidePropsContext) { 260 | return { 261 | redirect: { 262 | destination: route({ pathname: "/foos/[foo]", query: { foo: "foobar" } }), 263 | permanent: false, 264 | }, 265 | }; 266 | } 267 | ``` 268 | 269 | `route` optionally accepts a `trailingSlash`: 270 | 271 | ```ts 272 | // api/foos/foobar/ 273 | fetch( 274 | route( 275 | { pathname: "/api/foos/[foo]", query: { foo: "foobar" } }, 276 | { trailingSlash: true }, 277 | ), 278 | ); 279 | ``` 280 | 281 | ### Internationalization (i18n) 282 | 283 | `nextjs-routes` refines `Link` and `useRouter` based on your [Nextjs i18n configuration](https://nextjs.org/docs/advanced-features/i18n-routing). 284 | 285 | The following `next.config.js`: 286 | 287 | ```js 288 | module.exports = withRoutes({ 289 | i18n: { 290 | defaultLocale: "de-DE", 291 | locales: ["de-DE", "en-FR", "en-US"], 292 | }, 293 | }); 294 | ``` 295 | 296 | Will type `Link` and `useRouter`'s `locale` as `'de-DE' | 'en-FR' | 'en-US'`. All other i18n properties (`defaultLocale`, `domainLocales` and `locales`) are also typed. 297 | 298 | If you want to use the generated `Locale` type, you can import it from `nextjs-routes`: 299 | 300 | ```ts 301 | import { Locale } from "nextjs-routes"; 302 | ``` 303 | 304 | ## Configuration 305 | 306 | You can pass the following options to `nextRoutes` in your `next.config.js`: 307 | 308 | ```js 309 | const nextRoutes = require("nextjs-routes/config"); 310 | const withRoutes = nextRoutes({ 311 | outDir: "types", 312 | cwd: __dirname, 313 | }); 314 | ``` 315 | 316 | - `outDir`: The file path indicating the output directory where the generated route types should be written to (e.g.: "types"). The default is to create the file in the same folder as your `next.config.js` file. 317 | 318 | - `cwd`: The path to the directory that contains your `next.config.js` file. This is only necessary for non standard project structures, such as `nx`. If you are an `nx` user getting the `Could not find a Next.js pages directory` error, use `cwd: __dirname`. 319 | 320 | ## Troubleshooting 321 | 322 | ### Could not find a Next.js pages directory 323 | 324 | Non standard project structures, such as those using `nx`, require that users supply a path to their `next.config.js`. For `nx`, this is because `nx` introduces wrapping layers that invoke commands differently than using the `next` cli directly. 325 | 326 | Solution: 327 | 328 | ```diff 329 | const nextRoutes = require("nextjs-routes/config"); 330 | const withRoutes = nextRoutes({ 331 | + cwd: __dirname 332 | }); 333 | ``` 334 | 335 | ## Contributing 👫 336 | 337 | PR's and issues welcomed! For more guidance check out [CONTRIBUTING.md](https://github.com/tatethurston/nextjs-routes/blob/main/CONTRIBUTING.md) 338 | 339 | Are you interested in bringing a `nextjs-routes` like experience to another framework? [Open an issue](https://github.com/tatethurston/nextjs-routes/issues/new) and let's collaborate. 340 | 341 | ## Licensing 📃 342 | 343 | See the project's [MIT License](https://github.com/tatethurston/nextjs-routes/blob/main/LICENSE). 344 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 10% 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import typescriptPlugin from "@typescript-eslint/eslint-plugin"; 4 | import tsParser from "@typescript-eslint/parser"; 5 | 6 | export default [ 7 | { 8 | ignores: ["**/dist", "examples", "packages/e2e", "coverage"], 9 | }, 10 | js.configs.recommended, 11 | { 12 | plugins: { 13 | "@typescript-eslint": typescriptPlugin, 14 | }, 15 | languageOptions: { 16 | parser: tsParser, 17 | }, 18 | rules: { 19 | ...typescriptPlugin.configs.recommended.rules, 20 | }, 21 | }, 22 | { 23 | files: ["*.ts"], 24 | parser: "@typescript-eslint/parser", 25 | parserOptions: { 26 | // eslint-disable-next-line no-undef 27 | tsconfigRootDir: process.cwd(), 28 | project: [ 29 | "./tsconfig.json", 30 | "./packages/*/tsconfig.json", 31 | "./examples/*/tsconfig.json", 32 | ], 33 | }, 34 | rules: { 35 | ...typescriptPlugin.configs["recommended-requiring-type-checking"].rules, 36 | "@typescript-eslint/prefer-nullish-coalescing": "error", 37 | "@typescript-eslint/no-unnecessary-condition": "error", 38 | "@typescript-eslint/prefer-optional-chain": "error", 39 | }, 40 | }, 41 | eslintConfigPrettier, 42 | ]; 43 | -------------------------------------------------------------------------------- /examples/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /examples/app/@types/nextjs-routes.d.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | // This file will be automatically regenerated when your Next.js server is running. 3 | // nextjs-routes version: 2.2.5 4 | /* eslint-disable */ 5 | 6 | // prettier-ignore 7 | declare module "nextjs-routes" { 8 | import type { 9 | GetServerSidePropsContext as NextGetServerSidePropsContext, 10 | GetServerSidePropsResult as NextGetServerSidePropsResult 11 | } from "next"; 12 | 13 | export type Route = 14 | | StaticRoute<"/"> 15 | | DynamicRoute<"/[store]", { "store": string }>; 16 | 17 | interface StaticRoute { 18 | pathname: Pathname; 19 | query?: Query | undefined; 20 | hash?: string | null | undefined; 21 | } 22 | 23 | interface DynamicRoute { 24 | pathname: Pathname; 25 | query: Parameters & Query; 26 | hash?: string | null | undefined; 27 | } 28 | 29 | interface Query { 30 | [key: string]: string | string[] | undefined; 31 | }; 32 | 33 | export type RoutedQuery

= Extract< 34 | Route, 35 | { pathname: P } 36 | >["query"]; 37 | 38 | export type Locale = undefined; 39 | 40 | type Brand = K & { __brand: T }; 41 | 42 | /** 43 | * A string that is a valid application route. 44 | */ 45 | export type RouteLiteral = Brand 46 | 47 | /** 48 | * A typesafe utility function for generating paths in your application. 49 | * 50 | * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". 51 | */ 52 | export declare function route(r: Route): RouteLiteral; 53 | 54 | /** 55 | * Nearly identical to GetServerSidePropsContext from next, but further narrows 56 | * types based on nextjs-route's route data. 57 | */ 58 | export type GetServerSidePropsContext< 59 | Pathname extends Route["pathname"] = Route["pathname"], 60 | Preview extends NextGetServerSidePropsContext["previewData"] = NextGetServerSidePropsContext["previewData"] 61 | > = Omit & { 62 | params: Extract["query"]; 63 | query: Query; 64 | defaultLocale?: undefined; 65 | locale?: Locale; 66 | locales?: undefined; 67 | }; 68 | 69 | /** 70 | * Nearly identical to GetServerSideProps from next, but further narrows 71 | * types based on nextjs-route's route data. 72 | */ 73 | export type GetServerSideProps< 74 | Props extends { [key: string]: any } = { [key: string]: any }, 75 | Pathname extends Route["pathname"] = Route["pathname"], 76 | Preview extends NextGetServerSideProps["previewData"] = NextGetServerSideProps["previewData"] 77 | > = ( 78 | context: GetServerSidePropsContext 79 | ) => Promise> 80 | } 81 | 82 | // prettier-ignore 83 | declare module "next/link" { 84 | import type { Route, RouteLiteral } from "nextjs-routes";; 85 | import type { LinkProps as NextLinkProps } from "next/dist/client/link"; 86 | import type React from "react"; 87 | 88 | type StaticRoute = Exclude["pathname"]; 89 | 90 | export type LinkProps = Omit & { 91 | href: StaticRoute | RouteLiteral; 92 | locale?: false; 93 | } 94 | 95 | /** 96 | * A React component that extends the HTML `` element to provide [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 97 | * and client-side navigation between routes. 98 | * 99 | * It is the primary way to navigate between routes in Next.js. 100 | * 101 | * Read more: [Next.js docs: ``](https://nextjs.org/docs/app/api-reference/components/link) 102 | */ 103 | declare const Link: React.ForwardRefExoticComponent, keyof LinkProps> & LinkProps & { 104 | children?: React.ReactNode; 105 | } & React.RefAttributes>; 106 | export default Link; 107 | } 108 | 109 | // prettier-ignore 110 | declare module "next/router" { 111 | import type { Locale, Route, RoutedQuery } from "nextjs-routes"; 112 | import type { NextRouter as Router } from "next/dist/client/router"; 113 | export * from "next/dist/client/router"; 114 | export { default } from "next/dist/client/router"; 115 | 116 | type NextTransitionOptions = NonNullable[2]>; 117 | type StaticRoute = Exclude["pathname"]; 118 | 119 | interface TransitionOptions extends Omit { 120 | locale?: false; 121 | } 122 | 123 | type PathnameAndQuery = Required< 124 | Pick, "pathname" | "query"> 125 | >; 126 | 127 | type AutomaticStaticOptimizedQuery = Omit & { 128 | query: Partial; 129 | }; 130 | 131 | type BaseRouter = 132 | | ({ isReady: false } & AutomaticStaticOptimizedQuery) 133 | | ({ isReady: true } & PaQ); 134 | 135 | export type NextRouter

= 136 | BaseRouter> & 137 | Omit< 138 | Router, 139 | | "defaultLocale" 140 | | "domainLocales" 141 | | "isReady" 142 | | "locale" 143 | | "locales" 144 | | "pathname" 145 | | "push" 146 | | "query" 147 | | "replace" 148 | | "route" 149 | > & { 150 | defaultLocale?: undefined; 151 | domainLocales?: undefined; 152 | locale?: Locale; 153 | locales?: undefined; 154 | push( 155 | url: Route | StaticRoute | Omit, 156 | as?: string, 157 | options?: TransitionOptions 158 | ): Promise; 159 | replace( 160 | url: Route | StaticRoute | Omit, 161 | as?: string, 162 | options?: TransitionOptions 163 | ): Promise; 164 | route: P; 165 | }; 166 | 167 | export function useRouter

(): NextRouter

; 168 | } 169 | 170 | // prettier-ignore 171 | declare module "next/navigation" { 172 | export * from "next/dist/client/components/navigation"; 173 | import type { Route, RouteLiteral, RoutedQuery } from "nextjs-routes"; 174 | import type { AppRouterInstance as NextAppRouterInstance, NavigateOptions, PrefetchOptions } from "next/dist/shared/lib/app-router-context.shared-runtime"; 175 | 176 | type StaticRoute = Exclude["pathname"]; 177 | 178 | /** 179 | * A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook 180 | * that lets you read the current URL's pathname. 181 | * 182 | * @example 183 | * ```ts 184 | * "use client" 185 | * import { usePathname } from 'next/navigation' 186 | * 187 | * export default function Page() { 188 | * const pathname = usePathname() // returns "/dashboard" on /dashboard?foo=bar 189 | * // ... 190 | * } 191 | * ``` 192 | * 193 | * Read more: [Next.js Docs: `usePathname`](https://nextjs.org/docs/app/api-reference/functions/use-pathname) 194 | */ 195 | export const usePathname: () => RouteLiteral; 196 | 197 | type AppRouterInstance = Omit & { 198 | push(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void; 199 | replace(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void; 200 | prefetch(href: StaticRoute | RouteLiteral, options?: PrefetchOptions): void; 201 | } 202 | 203 | /** 204 | * 205 | * This hook allows you to programmatically change routes inside [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components). 206 | * 207 | * @example 208 | * ```ts 209 | * "use client" 210 | * import { useRouter } from 'next/navigation' 211 | * 212 | * export default function Page() { 213 | * const router = useRouter() 214 | * // ... 215 | * router.push('/dashboard') // Navigate to /dashboard 216 | * } 217 | * ``` 218 | * 219 | * Read more: [Next.js Docs: `useRouter`](https://nextjs.org/docs/app/api-reference/functions/use-router) 220 | */ 221 | export function useRouter(): AppRouterInstance; 222 | 223 | /** 224 | * A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook 225 | * that lets you read a route's dynamic params filled in by the current URL. 226 | * 227 | * @example 228 | * ```ts 229 | * "use client" 230 | * import { useParams } from 'next/navigation' 231 | * 232 | * export default function Page() { 233 | * // on /dashboard/[team] where pathname is /dashboard/nextjs 234 | * const { team } = useParams() // team === "nextjs" 235 | * } 236 | * ``` 237 | * 238 | * Read more: [Next.js Docs: `useParams`](https://nextjs.org/docs/app/api-reference/functions/use-params) 239 | */ 240 | export const useParams: () => RoutedQuery; 241 | } 242 | -------------------------------------------------------------------------------- /examples/app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```sh 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/app/eslint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next/core-web-vitals"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/app/next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextRoutes from "nextjs-routes/config"; 2 | 3 | const withRoutes = nextRoutes(); 4 | 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | }; 8 | 9 | export default withRoutes(nextConfig); 10 | -------------------------------------------------------------------------------- /examples/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "intl", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "next": "^15.2.2", 13 | "react": "19.0.0", 14 | "react-dom": "19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "22.13.10", 18 | "@types/react": "19.0.10", 19 | "eslint": "9.22.0", 20 | "eslint-config-next": "15.2.2", 21 | "nextjs-routes": "workspace:*", 22 | "typescript": "5.8.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatethurston/nextjs-routes/9fc0668c6ff1acfa69a54e6c1a434e350cf5b13a/examples/app/public/favicon.ico -------------------------------------------------------------------------------- /examples/app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/app/src/app/[store]/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | 5 | export default function Client() { 6 | const searchParams = useSearchParams(); 7 | const version = searchParams.get("v"); 8 | const store = searchParams.get("store"); 9 | 10 | return ( 11 |

12 |

Version: {version}

13 |

Store: {store}

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/app/src/app/[store]/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { readdir } from "node:fs/promises"; 3 | import Client from "./client"; 4 | 5 | export default async function Page() { 6 | const files = await readdir(".."); 7 | return ( 8 |
9 | 10 | Home 11 | {files.map((f) => ( 12 |

{f}

13 | ))} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/app/src/app/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // 'use client' marks this page as a Client Component 4 | // https://beta.nextjs.org/docs/rendering/server-and-client-components 5 | 6 | import { useRouter } from "next/navigation"; 7 | 8 | export default function Client() { 9 | const router = useRouter(); 10 | 11 | return ( 12 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/app/src/app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /examples/app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/app/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Client from "./client"; 3 | import { route } from "nextjs-routes"; 4 | 5 | export default function Page() { 6 | return ( 7 | <> 8 | 9 | Home 10 | 11 | Tate's Store 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /examples/cjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /examples/cjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```sh 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/cjs/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import Home from "../pages"; 3 | 4 | (global.fetch as any) = jest.fn(() => 5 | Promise.resolve({ 6 | json: () => Promise.resolve(JSON.stringify({ name: "Tate" })), 7 | }), 8 | ); 9 | 10 | describe("Home", () => { 11 | it("renders a Link", () => { 12 | render(); 13 | const link = screen.getByText("Foo"); 14 | // @ts-expect-error: https://github.com/testing-library/jest-dom/issues/546 15 | expect(link).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/cjs/eslint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next/core-web-vitals"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/cjs/jest.config.mjs: -------------------------------------------------------------------------------- 1 | // jest.config.mjs 2 | import nextJest from "next/jest.js"; 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: "./", 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | /** @type {import('jest').Config} */ 11 | const config = { 12 | setupFilesAfterEnv: ["/jest.setup.js"], 13 | testEnvironment: "jest-environment-jsdom", 14 | }; 15 | 16 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 17 | export default createJestConfig(config); 18 | -------------------------------------------------------------------------------- /examples/cjs/jest.setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect"; 2 | 3 | // https://github.com/vercel/next.js/issues/43769 4 | import { createContext } from "react"; 5 | jest.mock("next/dist/shared/lib/router-context.js", () => ({ 6 | RouterContext: createContext(true), 7 | })); 8 | -------------------------------------------------------------------------------- /examples/cjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/cjs/next.config.js: -------------------------------------------------------------------------------- 1 | const nextRoutes = require("nextjs-routes/config"); 2 | 3 | const withRoutes = nextRoutes({ 4 | // optional configuration: this will put the generated types in a types folder instead of at the project root. 5 | outDir: "types", 6 | }); 7 | 8 | const nextConfig = { 9 | reactStrictMode: true, 10 | }; 11 | 12 | module.exports = withRoutes(nextConfig); 13 | -------------------------------------------------------------------------------- /examples/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "cjs", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "next": "^15.2.2", 13 | "react": "19.0.0", 14 | "react-dom": "19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@testing-library/react": "^16.2.0", 18 | "@types/jest": "^29.5.14", 19 | "@types/node": "22.13.10", 20 | "@types/react": "19.0.10", 21 | "eslint": "^9.22.0", 22 | "eslint-config-next": "15.2.2", 23 | "jest": "^29.7.0", 24 | "jest-environment-jsdom": "^29.7.0", 25 | "nextjs-routes": "workspace:*", 26 | "typescript": "5.8.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/cjs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | 3 | export default function MyApp({ Component, pageProps }: AppProps) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /examples/cjs/pages/bars/[bar].tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import { useRouter } from "next/router"; 3 | 4 | const Bar: NextPage = () => { 5 | const router = useRouter<"/bars/[bar]">(); 6 | const { bar } = router.query; 7 | return
Bar: {bar}
; 8 | }; 9 | 10 | export default Bar; 11 | -------------------------------------------------------------------------------- /examples/cjs/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | 5 | const Home: NextPage = () => { 6 | return ( 7 | <> 8 | 9 | Create Next App 10 | 11 | 12 | 13 |
14 | 15 | Bar 16 | 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /examples/cjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatethurston/nextjs-routes/9fc0668c6ff1acfa69a54e6c1a434e350cf5b13a/examples/cjs/public/favicon.ico -------------------------------------------------------------------------------- /examples/cjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/cjs/types/nextjs-routes.d.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | // This file will be automatically regenerated when your Next.js server is running. 3 | // nextjs-routes version: 2.2.5 4 | /* eslint-disable */ 5 | 6 | // prettier-ignore 7 | declare module "nextjs-routes" { 8 | import type { 9 | GetServerSidePropsContext as NextGetServerSidePropsContext, 10 | GetServerSidePropsResult as NextGetServerSidePropsResult 11 | } from "next"; 12 | 13 | export type Route = 14 | | StaticRoute<"/"> 15 | | DynamicRoute<"/bars/[bar]", { "bar": string }>; 16 | 17 | interface StaticRoute { 18 | pathname: Pathname; 19 | query?: Query | undefined; 20 | hash?: string | null | undefined; 21 | } 22 | 23 | interface DynamicRoute { 24 | pathname: Pathname; 25 | query: Parameters & Query; 26 | hash?: string | null | undefined; 27 | } 28 | 29 | interface Query { 30 | [key: string]: string | string[] | undefined; 31 | }; 32 | 33 | export type RoutedQuery

= Extract< 34 | Route, 35 | { pathname: P } 36 | >["query"]; 37 | 38 | export type Locale = undefined; 39 | 40 | type Brand = K & { __brand: T }; 41 | 42 | /** 43 | * A string that is a valid application route. 44 | */ 45 | export type RouteLiteral = Brand 46 | 47 | /** 48 | * A typesafe utility function for generating paths in your application. 49 | * 50 | * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". 51 | */ 52 | export declare function route(r: Route): RouteLiteral; 53 | 54 | /** 55 | * Nearly identical to GetServerSidePropsContext from next, but further narrows 56 | * types based on nextjs-route's route data. 57 | */ 58 | export type GetServerSidePropsContext< 59 | Pathname extends Route["pathname"] = Route["pathname"], 60 | Preview extends NextGetServerSidePropsContext["previewData"] = NextGetServerSidePropsContext["previewData"] 61 | > = Omit & { 62 | params: Extract["query"]; 63 | query: Query; 64 | defaultLocale?: undefined; 65 | locale?: Locale; 66 | locales?: undefined; 67 | }; 68 | 69 | /** 70 | * Nearly identical to GetServerSideProps from next, but further narrows 71 | * types based on nextjs-route's route data. 72 | */ 73 | export type GetServerSideProps< 74 | Props extends { [key: string]: any } = { [key: string]: any }, 75 | Pathname extends Route["pathname"] = Route["pathname"], 76 | Preview extends NextGetServerSideProps["previewData"] = NextGetServerSideProps["previewData"] 77 | > = ( 78 | context: GetServerSidePropsContext 79 | ) => Promise> 80 | } 81 | 82 | // prettier-ignore 83 | declare module "next/link" { 84 | import type { Route } from "nextjs-routes";; 85 | import type { LinkProps as NextLinkProps } from "next/dist/client/link"; 86 | import type React from "react"; 87 | 88 | type StaticRoute = Exclude["pathname"]; 89 | 90 | export type LinkProps = Omit & { 91 | href: Route | StaticRoute | Omit; 92 | locale?: false; 93 | } 94 | 95 | /** 96 | * A React component that extends the HTML `` element to provide [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 97 | * and client-side navigation between routes. 98 | * 99 | * It is the primary way to navigate between routes in Next.js. 100 | * 101 | * Read more: [Next.js docs: ``](https://nextjs.org/docs/app/api-reference/components/link) 102 | */ 103 | declare const Link: React.ForwardRefExoticComponent, keyof LinkProps> & LinkProps & { 104 | children?: React.ReactNode; 105 | } & React.RefAttributes>; 106 | export default Link; 107 | } 108 | 109 | // prettier-ignore 110 | declare module "next/router" { 111 | import type { Locale, Route, RoutedQuery } from "nextjs-routes"; 112 | import type { NextRouter as Router } from "next/dist/client/router"; 113 | export * from "next/dist/client/router"; 114 | export { default } from "next/dist/client/router"; 115 | 116 | type NextTransitionOptions = NonNullable[2]>; 117 | type StaticRoute = Exclude["pathname"]; 118 | 119 | interface TransitionOptions extends Omit { 120 | locale?: false; 121 | } 122 | 123 | type PathnameAndQuery = Required< 124 | Pick, "pathname" | "query"> 125 | >; 126 | 127 | type AutomaticStaticOptimizedQuery = Omit & { 128 | query: Partial; 129 | }; 130 | 131 | type BaseRouter = 132 | | ({ isReady: false } & AutomaticStaticOptimizedQuery) 133 | | ({ isReady: true } & PaQ); 134 | 135 | export type NextRouter

= 136 | BaseRouter> & 137 | Omit< 138 | Router, 139 | | "defaultLocale" 140 | | "domainLocales" 141 | | "isReady" 142 | | "locale" 143 | | "locales" 144 | | "pathname" 145 | | "push" 146 | | "query" 147 | | "replace" 148 | | "route" 149 | > & { 150 | defaultLocale?: undefined; 151 | domainLocales?: undefined; 152 | locale?: Locale; 153 | locales?: undefined; 154 | push( 155 | url: Route | StaticRoute | Omit, 156 | as?: string, 157 | options?: TransitionOptions 158 | ): Promise; 159 | replace( 160 | url: Route | StaticRoute | Omit, 161 | as?: string, 162 | options?: TransitionOptions 163 | ): Promise; 164 | route: P; 165 | }; 166 | 167 | export function useRouter

(): NextRouter

; 168 | } 169 | -------------------------------------------------------------------------------- /examples/intl/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /examples/intl/@types/nextjs-routes.d.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | // This file will be automatically regenerated when your Next.js server is running. 3 | // nextjs-routes version: 2.2.5 4 | /* eslint-disable */ 5 | 6 | // prettier-ignore 7 | declare module "nextjs-routes" { 8 | import type { 9 | GetServerSidePropsContext as NextGetServerSidePropsContext, 10 | GetServerSidePropsResult as NextGetServerSidePropsResult 11 | } from "next"; 12 | 13 | export type Route = 14 | | StaticRoute<"/"> 15 | | DynamicRoute<"/[store]", { "store": string }>; 16 | 17 | interface StaticRoute { 18 | pathname: Pathname; 19 | query?: Query | undefined; 20 | hash?: string | null | undefined; 21 | } 22 | 23 | interface DynamicRoute { 24 | pathname: Pathname; 25 | query: Parameters & Query; 26 | hash?: string | null | undefined; 27 | } 28 | 29 | interface Query { 30 | [key: string]: string | string[] | undefined; 31 | }; 32 | 33 | export type RoutedQuery

= Extract< 34 | Route, 35 | { pathname: P } 36 | >["query"]; 37 | 38 | export type Locale = 39 | | "en-US" 40 | | "fr" 41 | | "nl-NL"; 42 | 43 | type Brand = K & { __brand: T }; 44 | 45 | /** 46 | * A string that is a valid application route. 47 | */ 48 | export type RouteLiteral = Brand 49 | 50 | /** 51 | * A typesafe utility function for generating paths in your application. 52 | * 53 | * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". 54 | */ 55 | export declare function route(r: Route): RouteLiteral; 56 | 57 | /** 58 | * Nearly identical to GetServerSidePropsContext from next, but further narrows 59 | * types based on nextjs-route's route data. 60 | */ 61 | export type GetServerSidePropsContext< 62 | Pathname extends Route["pathname"] = Route["pathname"], 63 | Preview extends NextGetServerSidePropsContext["previewData"] = NextGetServerSidePropsContext["previewData"] 64 | > = Omit & { 65 | params: Extract["query"]; 66 | query: Query; 67 | defaultLocale: "en-US"; 68 | locale: Locale; 69 | locales: [ 70 | "en-US", 71 | "fr", 72 | "nl-NL" 73 | ]; 74 | }; 75 | 76 | /** 77 | * Nearly identical to GetServerSideProps from next, but further narrows 78 | * types based on nextjs-route's route data. 79 | */ 80 | export type GetServerSideProps< 81 | Props extends { [key: string]: any } = { [key: string]: any }, 82 | Pathname extends Route["pathname"] = Route["pathname"], 83 | Preview extends NextGetServerSideProps["previewData"] = NextGetServerSideProps["previewData"] 84 | > = ( 85 | context: GetServerSidePropsContext 86 | ) => Promise> 87 | } 88 | 89 | // prettier-ignore 90 | declare module "next/link" { 91 | import type { Route } from "nextjs-routes";; 92 | import type { LinkProps as NextLinkProps } from "next/dist/client/link"; 93 | import type React from "react"; 94 | 95 | type StaticRoute = Exclude["pathname"]; 96 | 97 | export type LinkProps = Omit & { 98 | href: Route | StaticRoute | Omit; 99 | locale?: Locale | false; 100 | } 101 | 102 | /** 103 | * A React component that extends the HTML `` element to provide [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 104 | * and client-side navigation between routes. 105 | * 106 | * It is the primary way to navigate between routes in Next.js. 107 | * 108 | * Read more: [Next.js docs: ``](https://nextjs.org/docs/app/api-reference/components/link) 109 | */ 110 | declare const Link: React.ForwardRefExoticComponent, keyof LinkProps> & LinkProps & { 111 | children?: React.ReactNode; 112 | } & React.RefAttributes>; 113 | export default Link; 114 | } 115 | 116 | // prettier-ignore 117 | declare module "next/router" { 118 | import type { Locale, Route, RoutedQuery } from "nextjs-routes"; 119 | import type { NextRouter as Router } from "next/dist/client/router"; 120 | export * from "next/dist/client/router"; 121 | export { default } from "next/dist/client/router"; 122 | 123 | type NextTransitionOptions = NonNullable[2]>; 124 | type StaticRoute = Exclude["pathname"]; 125 | 126 | interface TransitionOptions extends Omit { 127 | locale?: Locale | false; 128 | } 129 | 130 | type PathnameAndQuery = Required< 131 | Pick, "pathname" | "query"> 132 | >; 133 | 134 | type AutomaticStaticOptimizedQuery = Omit & { 135 | query: Partial; 136 | }; 137 | 138 | type BaseRouter = 139 | | ({ isReady: false } & AutomaticStaticOptimizedQuery) 140 | | ({ isReady: true } & PaQ); 141 | 142 | export type NextRouter

= 143 | BaseRouter> & 144 | Omit< 145 | Router, 146 | | "defaultLocale" 147 | | "domainLocales" 148 | | "isReady" 149 | | "locale" 150 | | "locales" 151 | | "pathname" 152 | | "push" 153 | | "query" 154 | | "replace" 155 | | "route" 156 | > & { 157 | defaultLocale: "en-US"; 158 | domainLocales?: undefined; 159 | locale: Locale; 160 | locales: [ 161 | "en-US", 162 | "fr", 163 | "nl-NL" 164 | ]; 165 | push( 166 | url: Route | StaticRoute | Omit, 167 | as?: string, 168 | options?: TransitionOptions 169 | ): Promise; 170 | replace( 171 | url: Route | StaticRoute | Omit, 172 | as?: string, 173 | options?: TransitionOptions 174 | ): Promise; 175 | route: P; 176 | }; 177 | 178 | export function useRouter

(): NextRouter

; 179 | } 180 | -------------------------------------------------------------------------------- /examples/intl/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```sh 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/intl/eslint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next/core-web-vitals"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/intl/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/intl/next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextRoutes from "nextjs-routes/config"; 2 | 3 | const withRoutes = nextRoutes(); 4 | 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | i18n: { 8 | locales: ["en-US", "fr", "nl-NL"], 9 | defaultLocale: "en-US", 10 | }, 11 | }; 12 | 13 | export default withRoutes(nextConfig); 14 | -------------------------------------------------------------------------------- /examples/intl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "intl", 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "build": "next build", 8 | "dev": "next dev", 9 | "lint": "next lint", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "next": "^15.2.2", 14 | "react": "19.0.0", 15 | "react-dom": "19.0.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "22.13.10", 19 | "@types/react": "19.0.10", 20 | "eslint": "9.22.0", 21 | "eslint-config-next": "15.2.2", 22 | "nextjs-routes": "workspace:*", 23 | "typescript": "5.8.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/intl/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatethurston/nextjs-routes/9fc0668c6ff1acfa69a54e6c1a434e350cf5b13a/examples/intl/public/favicon.ico -------------------------------------------------------------------------------- /examples/intl/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/intl/src/pages/[store].tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/router"; 5 | 6 | const Home: NextPage = () => { 7 | const router = useRouter(); 8 | return ( 9 | <> 10 | 11 | Create Next App 12 | 13 | 14 | 15 |

16 | Home 17 | {router.locale === "en-US" ? ( 18 | 25 | ) : ( 26 | 35 | )} 36 |
37 | 38 | ); 39 | }; 40 | 41 | export default Home; 42 | -------------------------------------------------------------------------------- /examples/intl/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | 3 | function MyApp({ Component, pageProps }: AppProps) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /examples/intl/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | 5 | const Home: NextPage = () => { 6 | return ( 7 | <> 8 | 9 | Create Next App 10 | 11 | 12 | 13 |
14 | 15 | Tate's Store 16 | 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /examples/intl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /examples/typescript/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```sh 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/typescript/__test__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import Home from "../pages"; 3 | 4 | (global.fetch as any) = jest.fn(() => 5 | Promise.resolve({ 6 | json: () => Promise.resolve(JSON.stringify({ name: "Tate" })), 7 | }), 8 | ); 9 | 10 | describe("Home", () => { 11 | it("renders a Link", () => { 12 | render(); 13 | const link = screen.getByText("Foo"); 14 | // @ts-expect-error: https://github.com/testing-library/jest-dom/issues/546 15 | expect(link).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/typescript/eslint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next/core-web-vitals"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/typescript/jest.config.mjs: -------------------------------------------------------------------------------- 1 | // jest.config.mjs 2 | import nextJest from "next/jest.js"; 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: "./", 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | /** @type {import('jest').Config} */ 11 | const config = { 12 | setupFilesAfterEnv: ["/jest.setup.js"], 13 | testEnvironment: "jest-environment-jsdom", 14 | }; 15 | 16 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 17 | export default createJestConfig(config); 18 | -------------------------------------------------------------------------------- /examples/typescript/jest.setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect"; 2 | 3 | // https://github.com/vercel/next.js/issues/43769 4 | import { createContext } from "react"; 5 | jest.mock("next/dist/shared/lib/router-context.js", () => ({ 6 | RouterContext: createContext(true), 7 | })); 8 | -------------------------------------------------------------------------------- /examples/typescript/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/typescript/next.config.js: -------------------------------------------------------------------------------- 1 | const nextRoutes = require("nextjs-routes/config"); 2 | 3 | const withRoutes = nextRoutes({ 4 | // optional configuration: this will put the generated types in a types folder instead of at the project root. 5 | outDir: "types", 6 | }); 7 | 8 | const nextConfig = { 9 | reactStrictMode: true, 10 | }; 11 | 12 | module.exports = withRoutes(nextConfig); 13 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "typescript", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "next": "^15.2.2", 13 | "react": "19.0.0", 14 | "react-dom": "19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@testing-library/react": "^16.2.0", 18 | "@types/jest": "^29.5.14", 19 | "@types/node": "22.13.10", 20 | "@types/react": "19.0.10", 21 | "eslint": "9.22.0", 22 | "eslint-config-next": "15.2.2", 23 | "jest": "^29.7.0", 24 | "jest-environment-jsdom": "^29.7.0", 25 | "nextjs-routes": "workspace:*", 26 | "typescript": "5.8.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/typescript/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | 3 | function MyApp({ Component, pageProps }: AppProps) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /examples/typescript/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | type Data = { 5 | name: string; 6 | }; 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse, 11 | ) { 12 | res.status(200).json({ name: "John Doe" }); 13 | } 14 | -------------------------------------------------------------------------------- /examples/typescript/pages/bars/[bar].tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import { useRouter } from "next/router"; 3 | 4 | const Bar: NextPage = () => { 5 | const router = useRouter<"/bars/[bar]">(); 6 | const { bar } = router.query; 7 | return
Bar: {bar}
; 8 | }; 9 | 10 | export default Bar; 11 | -------------------------------------------------------------------------------- /examples/typescript/pages/foos/[foo].tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import { useRouter } from "next/router"; 3 | import { 4 | route, 5 | type GetServerSidePropsContext, 6 | type GetServerSideProps, 7 | } from "nextjs-routes"; 8 | 9 | const Foo: NextPage = () => { 10 | const router = useRouter<"/foos/[foo]">(); 11 | const { foo } = router.query; 12 | return
Foo: {foo}
; 13 | }; 14 | 15 | export const getServerSideProps = (async ( 16 | ctx: GetServerSidePropsContext<"/foos/[foo]">, 17 | ) => { 18 | return { 19 | redirect: { 20 | destination: route({ 21 | pathname: "/bars/[bar]", 22 | query: { bar: ctx.params.foo }, 23 | }), 24 | permanent: false, 25 | }, 26 | }; 27 | }) satisfies GetServerSideProps<{}, "/foos/[foo]">; 28 | 29 | export default Foo; 30 | -------------------------------------------------------------------------------- /examples/typescript/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import { route } from "nextjs-routes"; 5 | import { useEffect } from "react"; 6 | 7 | const Home: NextPage = () => { 8 | useEffect(() => { 9 | fetch(route({ pathname: "/api/hello" })) 10 | .then((r) => r.json()) 11 | .then(console.log); 12 | }, []); 13 | 14 | return ( 15 | <> 16 | 17 | Create Next App 18 | 19 | 20 | 21 |
22 | 23 | Foo 24 | 25 |
26 | 27 | ); 28 | }; 29 | 30 | export default Home; 31 | -------------------------------------------------------------------------------- /examples/typescript/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatethurston/nextjs-routes/9fc0668c6ff1acfa69a54e6c1a434e350cf5b13a/examples/typescript/public/favicon.ico -------------------------------------------------------------------------------- /examples/typescript/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/typescript/types/nextjs-routes.d.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | // This file will be automatically regenerated when your Next.js server is running. 3 | // nextjs-routes version: 2.2.5 4 | /* eslint-disable */ 5 | 6 | // prettier-ignore 7 | declare module "nextjs-routes" { 8 | import type { 9 | GetServerSidePropsContext as NextGetServerSidePropsContext, 10 | GetServerSidePropsResult as NextGetServerSidePropsResult 11 | } from "next"; 12 | 13 | export type Route = 14 | | StaticRoute<"/"> 15 | | StaticRoute<"/api/hello"> 16 | | DynamicRoute<"/bars/[bar]", { "bar": string }> 17 | | DynamicRoute<"/foos/[foo]", { "foo": string }>; 18 | 19 | interface StaticRoute { 20 | pathname: Pathname; 21 | query?: Query | undefined; 22 | hash?: string | null | undefined; 23 | } 24 | 25 | interface DynamicRoute { 26 | pathname: Pathname; 27 | query: Parameters & Query; 28 | hash?: string | null | undefined; 29 | } 30 | 31 | interface Query { 32 | [key: string]: string | string[] | undefined; 33 | }; 34 | 35 | export type RoutedQuery

= Extract< 36 | Route, 37 | { pathname: P } 38 | >["query"]; 39 | 40 | export type Locale = undefined; 41 | 42 | type Brand = K & { __brand: T }; 43 | 44 | /** 45 | * A string that is a valid application route. 46 | */ 47 | export type RouteLiteral = Brand 48 | 49 | /** 50 | * A typesafe utility function for generating paths in your application. 51 | * 52 | * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". 53 | */ 54 | export declare function route(r: Route): RouteLiteral; 55 | 56 | /** 57 | * Nearly identical to GetServerSidePropsContext from next, but further narrows 58 | * types based on nextjs-route's route data. 59 | */ 60 | export type GetServerSidePropsContext< 61 | Pathname extends Route["pathname"] = Route["pathname"], 62 | Preview extends NextGetServerSidePropsContext["previewData"] = NextGetServerSidePropsContext["previewData"] 63 | > = Omit & { 64 | params: Extract["query"]; 65 | query: Query; 66 | defaultLocale?: undefined; 67 | locale?: Locale; 68 | locales?: undefined; 69 | }; 70 | 71 | /** 72 | * Nearly identical to GetServerSideProps from next, but further narrows 73 | * types based on nextjs-route's route data. 74 | */ 75 | export type GetServerSideProps< 76 | Props extends { [key: string]: any } = { [key: string]: any }, 77 | Pathname extends Route["pathname"] = Route["pathname"], 78 | Preview extends NextGetServerSideProps["previewData"] = NextGetServerSideProps["previewData"] 79 | > = ( 80 | context: GetServerSidePropsContext 81 | ) => Promise> 82 | } 83 | 84 | // prettier-ignore 85 | declare module "next/link" { 86 | import type { Route } from "nextjs-routes";; 87 | import type { LinkProps as NextLinkProps } from "next/dist/client/link"; 88 | import type React from "react"; 89 | 90 | type StaticRoute = Exclude["pathname"]; 91 | 92 | export type LinkProps = Omit & { 93 | href: Route | StaticRoute | Omit; 94 | locale?: false; 95 | } 96 | 97 | /** 98 | * A React component that extends the HTML `` element to provide [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 99 | * and client-side navigation between routes. 100 | * 101 | * It is the primary way to navigate between routes in Next.js. 102 | * 103 | * Read more: [Next.js docs: ``](https://nextjs.org/docs/app/api-reference/components/link) 104 | */ 105 | declare const Link: React.ForwardRefExoticComponent, keyof LinkProps> & LinkProps & { 106 | children?: React.ReactNode; 107 | } & React.RefAttributes>; 108 | export default Link; 109 | } 110 | 111 | // prettier-ignore 112 | declare module "next/router" { 113 | import type { Locale, Route, RoutedQuery } from "nextjs-routes"; 114 | import type { NextRouter as Router } from "next/dist/client/router"; 115 | export * from "next/dist/client/router"; 116 | export { default } from "next/dist/client/router"; 117 | 118 | type NextTransitionOptions = NonNullable[2]>; 119 | type StaticRoute = Exclude["pathname"]; 120 | 121 | interface TransitionOptions extends Omit { 122 | locale?: false; 123 | } 124 | 125 | type PathnameAndQuery = Required< 126 | Pick, "pathname" | "query"> 127 | >; 128 | 129 | type AutomaticStaticOptimizedQuery = Omit & { 130 | query: Partial; 131 | }; 132 | 133 | type BaseRouter = 134 | | ({ isReady: false } & AutomaticStaticOptimizedQuery) 135 | | ({ isReady: true } & PaQ); 136 | 137 | export type NextRouter

= 138 | BaseRouter> & 139 | Omit< 140 | Router, 141 | | "defaultLocale" 142 | | "domainLocales" 143 | | "isReady" 144 | | "locale" 145 | | "locales" 146 | | "pathname" 147 | | "push" 148 | | "query" 149 | | "replace" 150 | | "route" 151 | > & { 152 | defaultLocale?: undefined; 153 | domainLocales?: undefined; 154 | locale?: Locale; 155 | locales?: undefined; 156 | push( 157 | url: Route | StaticRoute | Omit, 158 | as?: string, 159 | options?: TransitionOptions 160 | ): Promise; 161 | replace( 162 | url: Route | StaticRoute | Omit, 163 | as?: string, 164 | options?: TransitionOptions 165 | ): Promise; 166 | route: P; 167 | }; 168 | 169 | export function useRouter

(): NextRouter

; 170 | } 171 | -------------------------------------------------------------------------------- /images/nextjs-routes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tatethurston/nextjs-routes/9fc0668c6ff1acfa69a54e6c1a434e350cf5b13a/images/nextjs-routes.gif -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: "coverage", 4 | modulePathIgnorePatterns: ["dist", "examples"], 5 | // TS ESM imports are referenced with .js extensions, but jest will fail to find 6 | // the uncompiled file because it ends with .ts and is looking for .js. 7 | moduleNameMapper: { 8 | "(.+)\\.jsx?": "$1", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "nextjs-routes-development", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "author": "Tate ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/tatethurston/nextjs-routes" 10 | }, 11 | "type": "module", 12 | "scripts": { 13 | "build": "pnpm --filter nextjs-routes run package:build", 14 | "e2e:setup": "pnpm build && (cd packages/e2e && pnpm next build)", 15 | "examples:each": "for e in ./examples/*; do (cd \"$e\" && $npm_config_command); done", 16 | "examples:regen": "npm run examples:each --command='pnpm next build'", 17 | "lint": "pnpm typecheck && prettier --check . && eslint . && pnpm prettier-package-json --list-different './{packages,examples}/*/package.json'", 18 | "lint:fix": "prettier --write '**/*.{ts,cts,mts,tsx,md,json,yaml}' && prettier-package-json --write './{packages,exmaples}/*/package.json' && eslint --fix .", 19 | "prepare": "husky install", 20 | "test": "pnpm run e2e:setup && jest", 21 | "test:ci": "pnpm run test --coverage", 22 | "typecheck": "pnpm --recursive exec tsc --noEmit" 23 | }, 24 | "sideEffects": false, 25 | "devDependencies": { 26 | "@babel/preset-env": "^7.26.9", 27 | "@babel/preset-typescript": "^7.26.0", 28 | "@eslint/js": "^9.22.0", 29 | "@swc/core": "^1.11.9", 30 | "@types/jest": "^29.5.14", 31 | "@types/mock-fs": "^4.13.4", 32 | "@types/node": "^22.13.10", 33 | "@types/react": "^19.0.10", 34 | "@types/webpack": "^5.28.5", 35 | "@typescript-eslint/eslint-plugin": "^8.26.1", 36 | "@typescript-eslint/parser": "^8.26.1", 37 | "babel-loader": "^10.0.0", 38 | "codecov": "^3.8.3", 39 | "eslint": "^9.22.0", 40 | "eslint-config-prettier": "^10.1.1", 41 | "husky": "^9.1.7", 42 | "jest": "^29.7.0", 43 | "mock-fs": "^5.5.0", 44 | "next": "^15.2.2", 45 | "prettier": "^3.5.3", 46 | "prettier-package-json": "^2.8.0", 47 | "swc-loader": "^0.2.6", 48 | "typescript": "^5.8.2", 49 | "webpack": "^5.98.0", 50 | "webpack-cli": "^6.0.1" 51 | }, 52 | "packageManager": "pnpm@9.10.0" 53 | } 54 | -------------------------------------------------------------------------------- /packages/e2e/@types/nextjs-routes.d.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | // This file will be automatically regenerated when your Next.js server is running. 3 | // nextjs-routes version: 2.2.5 4 | /* eslint-disable */ 5 | 6 | // prettier-ignore 7 | declare module "nextjs-routes" { 8 | import type { 9 | GetServerSidePropsContext as NextGetServerSidePropsContext, 10 | GetServerSidePropsResult as NextGetServerSidePropsResult 11 | } from "next"; 12 | 13 | export type Route = 14 | | StaticRoute<"/"> 15 | | DynamicRoute<"/[...slug]", { "slug": string[] }> 16 | | DynamicRoute<"/bars/[bar]", { "bar": string }> 17 | | DynamicRoute<"/foos/[foo]", { "foo": string }>; 18 | 19 | interface StaticRoute { 20 | pathname: Pathname; 21 | query?: Query | undefined; 22 | hash?: string | null | undefined; 23 | } 24 | 25 | interface DynamicRoute { 26 | pathname: Pathname; 27 | query: Parameters & Query; 28 | hash?: string | null | undefined; 29 | } 30 | 31 | interface Query { 32 | [key: string]: string | string[] | undefined; 33 | }; 34 | 35 | export type RoutedQuery

= Extract< 36 | Route, 37 | { pathname: P } 38 | >["query"]; 39 | 40 | export type Locale = undefined; 41 | 42 | type Brand = K & { __brand: T }; 43 | 44 | /** 45 | * A string that is a valid application route. 46 | */ 47 | export type RouteLiteral = Brand 48 | 49 | /** 50 | * A typesafe utility function for generating paths in your application. 51 | * 52 | * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". 53 | */ 54 | export declare function route(r: Route): RouteLiteral; 55 | 56 | /** 57 | * Nearly identical to GetServerSidePropsContext from next, but further narrows 58 | * types based on nextjs-route's route data. 59 | */ 60 | export type GetServerSidePropsContext< 61 | Pathname extends Route["pathname"] = Route["pathname"], 62 | Preview extends NextGetServerSidePropsContext["previewData"] = NextGetServerSidePropsContext["previewData"] 63 | > = Omit & { 64 | params: Extract["query"]; 65 | query: Query; 66 | defaultLocale?: undefined; 67 | locale?: Locale; 68 | locales?: undefined; 69 | }; 70 | 71 | /** 72 | * Nearly identical to GetServerSideProps from next, but further narrows 73 | * types based on nextjs-route's route data. 74 | */ 75 | export type GetServerSideProps< 76 | Props extends { [key: string]: any } = { [key: string]: any }, 77 | Pathname extends Route["pathname"] = Route["pathname"], 78 | Preview extends NextGetServerSideProps["previewData"] = NextGetServerSideProps["previewData"] 79 | > = ( 80 | context: GetServerSidePropsContext 81 | ) => Promise> 82 | } 83 | 84 | // prettier-ignore 85 | declare module "next/link" { 86 | import type { Route, RouteLiteral } from "nextjs-routes";; 87 | import type { LinkProps as NextLinkProps } from "next/dist/client/link"; 88 | import type React from "react"; 89 | 90 | type StaticRoute = Exclude["pathname"]; 91 | 92 | export type LinkProps = Omit & { 93 | href: Route | StaticRoute | Omit | RouteLiteral; 94 | locale?: false; 95 | } 96 | 97 | /** 98 | * A React component that extends the HTML `` element to provide [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 99 | * and client-side navigation between routes. 100 | * 101 | * It is the primary way to navigate between routes in Next.js. 102 | * 103 | * Read more: [Next.js docs: ``](https://nextjs.org/docs/app/api-reference/components/link) 104 | */ 105 | declare const Link: React.ForwardRefExoticComponent, keyof LinkProps> & LinkProps & { 106 | children?: React.ReactNode; 107 | } & React.RefAttributes>; 108 | export default Link; 109 | } 110 | 111 | // prettier-ignore 112 | declare module "next/router" { 113 | import type { Locale, Route, RoutedQuery } from "nextjs-routes"; 114 | import type { NextRouter as Router } from "next/dist/client/router"; 115 | export * from "next/dist/client/router"; 116 | export { default } from "next/dist/client/router"; 117 | 118 | type NextTransitionOptions = NonNullable[2]>; 119 | type StaticRoute = Exclude["pathname"]; 120 | 121 | interface TransitionOptions extends Omit { 122 | locale?: false; 123 | } 124 | 125 | type PathnameAndQuery = Required< 126 | Pick, "pathname" | "query"> 127 | >; 128 | 129 | type AutomaticStaticOptimizedQuery = Omit & { 130 | query: Partial; 131 | }; 132 | 133 | type BaseRouter = 134 | | ({ isReady: false } & AutomaticStaticOptimizedQuery) 135 | | ({ isReady: true } & PaQ); 136 | 137 | export type NextRouter

= 138 | BaseRouter> & 139 | Omit< 140 | Router, 141 | | "defaultLocale" 142 | | "domainLocales" 143 | | "isReady" 144 | | "locale" 145 | | "locales" 146 | | "pathname" 147 | | "push" 148 | | "query" 149 | | "replace" 150 | | "route" 151 | > & { 152 | defaultLocale?: undefined; 153 | domainLocales?: undefined; 154 | locale?: Locale; 155 | locales?: undefined; 156 | push( 157 | url: Route | StaticRoute | Omit, 158 | as?: string, 159 | options?: TransitionOptions 160 | ): Promise; 161 | replace( 162 | url: Route | StaticRoute | Omit, 163 | as?: string, 164 | options?: TransitionOptions 165 | ): Promise; 166 | route: P; 167 | }; 168 | 169 | export function useRouter

(): NextRouter

; 170 | } 171 | 172 | // prettier-ignore 173 | declare module "next/navigation" { 174 | export * from "next/dist/client/components/navigation"; 175 | import type { Route, RouteLiteral, RoutedQuery } from "nextjs-routes"; 176 | import type { AppRouterInstance as NextAppRouterInstance, NavigateOptions, PrefetchOptions } from "next/dist/shared/lib/app-router-context.shared-runtime"; 177 | 178 | type StaticRoute = Exclude["pathname"]; 179 | 180 | /** 181 | * A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook 182 | * that lets you read the current URL's pathname. 183 | * 184 | * @example 185 | * ```ts 186 | * "use client" 187 | * import { usePathname } from 'next/navigation' 188 | * 189 | * export default function Page() { 190 | * const pathname = usePathname() // returns "/dashboard" on /dashboard?foo=bar 191 | * // ... 192 | * } 193 | * ``` 194 | * 195 | * Read more: [Next.js Docs: `usePathname`](https://nextjs.org/docs/app/api-reference/functions/use-pathname) 196 | */ 197 | export const usePathname: () => RouteLiteral; 198 | 199 | type AppRouterInstance = Omit & { 200 | push(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void; 201 | replace(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void; 202 | prefetch(href: StaticRoute | RouteLiteral, options?: PrefetchOptions): void; 203 | } 204 | 205 | /** 206 | * 207 | * This hook allows you to programmatically change routes inside [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components). 208 | * 209 | * @example 210 | * ```ts 211 | * "use client" 212 | * import { useRouter } from 'next/navigation' 213 | * 214 | * export default function Page() { 215 | * const router = useRouter() 216 | * // ... 217 | * router.push('/dashboard') // Navigate to /dashboard 218 | * } 219 | * ``` 220 | * 221 | * Read more: [Next.js Docs: `useRouter`](https://nextjs.org/docs/app/api-reference/functions/use-router) 222 | */ 223 | export function useRouter(): AppRouterInstance; 224 | 225 | /** 226 | * A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook 227 | * that lets you read a route's dynamic params filled in by the current URL. 228 | * 229 | * @example 230 | * ```ts 231 | * "use client" 232 | * import { useParams } from 'next/navigation' 233 | * 234 | * export default function Page() { 235 | * // on /dashboard/[team] where pathname is /dashboard/nextjs 236 | * const { team } = useParams() // team === "nextjs" 237 | * } 238 | * ``` 239 | * 240 | * Read more: [Next.js Docs: `useParams`](https://nextjs.org/docs/app/api-reference/functions/use-params) 241 | */ 242 | export const useParams: () => RoutedQuery; 243 | } 244 | -------------------------------------------------------------------------------- /packages/e2e/app/bars/[bar]/page.tsx: -------------------------------------------------------------------------------- 1 | export default () =>

Bar
; 2 | -------------------------------------------------------------------------------- /packages/e2e/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/e2e/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | 3 | function run(cmd: string) { 4 | return spawnSync(cmd, { shell: true, encoding: "utf8" }); 5 | } 6 | 7 | describe("e2e", () => { 8 | process.chdir(__dirname); 9 | 10 | it.each(["pnpm next build", "pnpm tsc --noEmit"])("%s", (cmd) => { 11 | const result = run(cmd); 12 | if (result.status !== 0) { 13 | console.log(result.output); 14 | } 15 | expect(result.status).toEqual(0); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/e2e/eslint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next/core-web-vitals"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/e2e/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 7 | -------------------------------------------------------------------------------- /packages/e2e/next.config.js: -------------------------------------------------------------------------------- 1 | const nextRoutes = require("nextjs-routes/config"); 2 | 3 | const withRoutes = nextRoutes(); 4 | 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | }; 8 | 9 | module.exports = withRoutes(nextConfig); 10 | -------------------------------------------------------------------------------- /packages/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "next build", 8 | "dev": "next dev", 9 | "lint": "next lint", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "next": "^15.2.2", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0" 16 | }, 17 | "devDependencies": { 18 | "nextjs-routes": "workspace:*", 19 | "typescript": "^5.8.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/e2e/pages/[...slug].tsx: -------------------------------------------------------------------------------- 1 | export default function () { 2 |
; 3 | } 4 | -------------------------------------------------------------------------------- /packages/e2e/pages/foos/[foo].tsx: -------------------------------------------------------------------------------- 1 | export default () =>
Foo
; 2 | -------------------------------------------------------------------------------- /packages/e2e/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { route } from "nextjs-routes"; 2 | 3 | export default () =>
Index: {route({ pathname: "/" })}
; 4 | -------------------------------------------------------------------------------- /packages/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/e2e/typetest.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { LinkProps } from "next/link"; 3 | import { useRouter, RouterEvent, NextRouter } from "next/router"; 4 | import { 5 | usePathname, 6 | useRouter as useAppRouter, 7 | useParams, 8 | } from "next/navigation"; 9 | import { 10 | route, 11 | type Route, 12 | type RoutedQuery, 13 | type GetServerSideProps, 14 | type GetServerSidePropsContext, 15 | RouteLiteral, 16 | } from "nextjs-routes"; 17 | import nextRoutes from "nextjs-routes/config"; 18 | 19 | nextRoutes(); 20 | nextRoutes({}); 21 | nextRoutes({ outDir: "types" }); 22 | // @ts-expect-error invalid key 'foo' 23 | nextRoutes({ foo: false }); 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function 26 | function expectType(_value: T) {} 27 | 28 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function 29 | function expectNotAny(_value: NotAny) {} 30 | 31 | type NotAny = unknown extends T ? never : T; 32 | 33 | // next/link 34 | 35 | // Links with string hrefs 36 | ; 37 | // @ts-expect-error dynamic paths are only valid with a UrlObject 38 | ; 39 | // @ts-expect-error bar isn't a valid path name 40 | ; 41 | 42 | // Path without dynamic segments 43 | ; 44 | ; 45 | ; 46 | ; 47 | ; 48 | ; 49 | ; 50 | 51 | // Path with dynamic segments 52 | ; 53 | // @ts-expect-error missing 'foo' in query 54 | ; 55 | // @ts-expect-error missing 'foo' in query 56 | ; 57 | // @ts-expect-error missing 'foo' in query 58 | ; 59 | ; 60 | // @ts-expect-error 'foo' must be a string, not string[] 61 | ; 62 | 63 | // Catch All 64 | ; 65 | // @ts-expect-error missing 'slug' in query 66 | ; 67 | 68 | // Change query for current page 69 | ; 70 | ; 71 | ; 72 | 73 | // Change hash for current page 74 | ; 75 | 76 | // Change hash and query for current page 77 | ; 78 | 79 | // Unaugmented props 80 | ; 93 | // @ts-expect-error replace typo 94 | ; 95 | // anchor props https://beta.nextjs.org/docs/api-reference/components/link#props 96 | ; 97 | {}} />; 98 | 99 | // LinkProps 100 | 101 | // ensure LinkProps is typed (Next.js route types are overriden by nextjs-routes.d.ts) 102 | expectType({ pathname: "/" }); 103 | // static route 104 | expectType("/"); 105 | // @ts-expect-error invalid pathname 106 | expectType({ pathname: "/invalid" }); 107 | 108 | // next/router 109 | const router = useRouter(); 110 | 111 | // pathname 112 | 113 | expectType<"/" | "/foos/[foo]" | "/bars/[bar]" | "/[...slug]">(router.pathname); 114 | 115 | // route 116 | 117 | expectType<"/" | "/foos/[foo]" | "/bars/[bar]" | "/[...slug]">(router.route); 118 | 119 | // query 120 | 121 | expectType(router.query.foo); 122 | expectType(router.query.bar); 123 | // type narrowing 124 | const router1 = useRouter<"/foos/[foo]">(); 125 | expectType(router1.query.foo); 126 | if (router1.isReady) { 127 | expectType(router1.query.foo); 128 | } 129 | 130 | // push 131 | 132 | // Path without dynamic segments 133 | router.push({ pathname: "/" }); 134 | router.push({ pathname: "/", query: undefined }); 135 | router.push({ pathname: "/", query: {} }); 136 | router.push({ pathname: "/", query: { bar: "baz" } }); 137 | router.push({ pathname: "/", query: { bar: ["foo", "baz"] } }); 138 | router.push({ pathname: "/", query: { bar: ["foo", "baz"] }, hash: "foo" }); 139 | 140 | // Path with dynamic segments 141 | router.push({ pathname: "/foos/[foo]", query: { foo: "baz" } }); 142 | // @ts-expect-error missing 'foo' in query 143 | router.push({ pathname: "/foos/[foo]", query: { bar: "baz" } }); 144 | // @ts-expect-error missing 'foo' in query 145 | router.push({ pathname: "/foos/[foo]", query: undefined }); 146 | // @ts-expect-error missing 'foo' in query 147 | router.push({ pathname: "/foos/[foo]", query: {} }); 148 | router.push({ pathname: "/foos/[foo]", query: { foo: "baz", bar: "baz" } }); 149 | // @ts-expect-error 'foo' must be a string, not string[] 150 | router.push({ pathname: "/foos/[foo]", query: { foo: ["bar", "baz"] } }); 151 | 152 | // Catch All 153 | router.push({ pathname: "/[...slug]", query: { slug: ["baz", "foo"] } }); 154 | // @ts-expect-error missing 'slug' in query 155 | router.push({ pathname: "/[...slug]", query: { slug: undefined } }); 156 | 157 | // Change query for current page 158 | router.push({ query: { bar: "baz" } }); 159 | router.push({ query: { foo: "foo" } }); 160 | router.push({ query: { foo: ["foo", "bar"] } }); 161 | 162 | // Change hash for current page 163 | router.push({ hash: "#foo" }); 164 | 165 | // Change hash and query for current page 166 | router.push({ query: { bar: "baz" }, hash: "#foo" }); 167 | 168 | // Reassignment 169 | router.push(router); 170 | 171 | // Unaugmented options 172 | router.push({}, undefined, { shallow: true, locale: false, scroll: true }); 173 | router.push({ query: {} }, undefined, { 174 | shallow: true, 175 | locale: false, 176 | scroll: true, 177 | }); 178 | // @ts-expect-error shallow typo 179 | router.push({ query: {} }, undefined, { shallowy: true }); 180 | 181 | // replace 182 | 183 | // Path without dynamic segments 184 | router.replace({ pathname: "/" }); 185 | router.replace({ pathname: "/", query: undefined }); 186 | router.replace({ pathname: "/", query: {} }); 187 | router.replace({ pathname: "/", query: { bar: "baz" } }); 188 | router.replace({ pathname: "/", query: { bar: ["foo", "baz"] } }); 189 | router.replace({ pathname: "/", query: { bar: ["foo", "baz"] }, hash: "foo" }); 190 | 191 | // Path with dynamic segments 192 | router.replace({ pathname: "/foos/[foo]", query: { foo: "baz" } }); 193 | // @ts-expect-error missing 'foo' in query 194 | router.replace({ pathname: "/foos/[foo]", query: { bar: "baz" } }); 195 | // @ts-expect-error missing 'foo' in query 196 | router.replace({ pathname: "/foos/[foo]", query: undefined }); 197 | // @ts-expect-error missing 'foo' in query 198 | router.replace({ pathname: "/foos/[foo]", query: {} }); 199 | router.replace({ pathname: "/foos/[foo]", query: { foo: "baz", bar: "baz" } }); 200 | // @ts-expect-error 'foo' must be a string, not string[] 201 | router.replace({ pathname: "/foos/[foo]", query: { foo: ["bar", "baz"] } }); 202 | 203 | // Only change query for current page 204 | router.replace({ query: { bar: "baz" } }); 205 | router.replace({ query: { foo: "foo" } }); 206 | router.replace({ query: { foo: ["bar", "baz"] } }); 207 | 208 | // Unaugmented options 209 | router.replace({}, undefined, { shallow: true, locale: false, scroll: true }); 210 | router.replace({ query: {} }, undefined, { 211 | shallow: true, 212 | locale: false, 213 | scroll: true, 214 | }); 215 | // @ts-expect-error shallow typo 216 | router.replace({ query: {} }, undefined, { shallowy: true }); 217 | 218 | // Reassignment 219 | router.replace(router); 220 | router.replace({ query: router.query }); 221 | 222 | // RouterEvent 223 | 224 | let routerEvent: RouterEvent; 225 | 226 | routerEvent = "routeChangeStart"; 227 | // @ts-expect-error event typo 228 | routerEvent = "routeChangeStarty"; 229 | 230 | // NextRouter 231 | 232 | // ensure NextRouter is our NextRouter, not the untyped one 233 | 234 | let nextRouter: NextRouter = undefined as unknown as NextRouter; 235 | 236 | nextRouter.push({ pathname: "/" }); 237 | 238 | // @ts-expect-error invalid pathname 239 | nextRouter.push({ pathname: "/invalid" }); 240 | 241 | // nextjs-routes 242 | 243 | // Route 244 | 245 | let r: Route; 246 | 247 | // Path without dynamic segments 248 | r = { pathname: "/" }; 249 | r = { pathname: "/", query: undefined }; 250 | r = { pathname: "/", query: {} }; 251 | r = { pathname: "/", query: { bar: "baz" } }; 252 | r = { pathname: "/", query: { bar: ["foo", "baz"] } }; 253 | 254 | // Path with dynamic segments 255 | r = { pathname: "/foos/[foo]", query: { foo: "baz" } }; 256 | // @ts-expect-error missing 'foo' in query 257 | r = { pathname: "/foos/[foo]", query: { bar: "baz" } }; 258 | // @ts-expect-error missing 'foo' in query 259 | r = { pathname: "/foos/[foo]", query: undefined }; 260 | // @ts-expect-error missing 'foo' in query 261 | r = { pathname: "/foos/[foo]", query: {} }; 262 | r = { pathname: "/foos/[foo]", query: { foo: "baz", bar: "baz" } }; 263 | // @ts-expect-error 'foo' must be a string, not string[] 264 | r = { pathname: "/foos/[foo]", query: { foo: ["bar", "baz"] } }; 265 | 266 | // route 267 | // Path without dynamic segments 268 | route({ pathname: "/" }); 269 | // Path with dynamic segments 270 | route({ pathname: "/foos/[foo]", query: { foo: "baz" } }); 271 | route({ pathname: "/foos/[foo]", query: { foo: "baz" }, hash: "foo" }); 272 | // @ts-expect-error missing 'foo' in query 273 | route({ pathname: "/foos/[foo]", query: { bar: "baz" } }); 274 | // @ts-expect-error 'foo' must be a string, not string[] 275 | route({ pathname: "/foos/[foo]", query: { foo: ["bar", "baz"] } }); 276 | 277 | // RoutedQuery 278 | 279 | // Path without dynamic segments 280 | let rq1: RoutedQuery<"/">; 281 | rq1 = {}; 282 | rq1 = { foo: "baz" }; 283 | rq1 = { foo: ["bar", "baz"] }; 284 | rq1 = { foo: undefined }; 285 | 286 | // Path with dynamic segments 287 | let rq2: RoutedQuery<"/foos/[foo]">; 288 | // @ts-expect-error missing 'foo' in query 289 | rq2 = {}; 290 | rq2 = { foo: "bar" }; 291 | // @ts-expect-error missing 'foo' in query 292 | rq2 = { bar: "baz" }; 293 | // @ts-expect-error 'foo' must be a string, not string[] 294 | rq2 = { foo: ["bar", "baz"] }; 295 | 296 | let getServerSideProps = (async ( 297 | ctx: GetServerSidePropsContext<"/foos/[foo]">, 298 | ) => { 299 | expectType(ctx.params.foo); 300 | return { 301 | redirect: { 302 | destination: route({ 303 | pathname: "/foos/[foo]", 304 | query: { foo: ctx.params.foo }, 305 | }), 306 | permanent: false, 307 | }, 308 | }; 309 | }) satisfies GetServerSideProps<{}, "/foos/[foo]">; 310 | 311 | expectType< 312 | ( 313 | ctx: GetServerSidePropsContext<"/foos/[foo]">, 314 | ) => Promise<{ redirect: { destination: string; permanent: boolean } }> 315 | >(getServerSideProps); 316 | 317 | getServerSideProps = (async (ctx) => { 318 | expectType(ctx.params.foo); 319 | return { 320 | redirect: { 321 | destination: route({ 322 | pathname: "/foos/[foo]", 323 | query: { foo: ctx.params.foo }, 324 | }), 325 | permanent: false, 326 | }, 327 | }; 328 | }) satisfies GetServerSideProps<{}, "/foos/[foo]">; 329 | 330 | // next/navigation 331 | interface NavigateOptions { 332 | scroll?: boolean; 333 | } 334 | 335 | enum PrefetchKind { 336 | AUTO = "auto", 337 | FULL = "full", 338 | TEMPORARY = "temporary", 339 | } 340 | 341 | interface PrefetchOptions { 342 | kind: PrefetchKind; 343 | } 344 | expectType<() => void>(useAppRouter().back); 345 | expectType<() => void>(useAppRouter().forward); 346 | expectType<() => void>(useAppRouter().refresh); 347 | expectType<(href: RouteLiteral, options: NavigateOptions) => void>( 348 | useAppRouter().push, 349 | ); 350 | expectType<(href: RouteLiteral, options: NavigateOptions) => void>( 351 | useAppRouter().replace, 352 | ); 353 | expectType<(href: string, options: PrefetchOptions) => void>( 354 | useAppRouter().prefetch, 355 | ); 356 | 357 | expectNotAny(usePathname()); 358 | expectType(usePathname()); 359 | 360 | expectNotAny(useParams()); 361 | expectType(useParams()); 362 | expectType>(useParams<"/bars/[bar]">()); 363 | -------------------------------------------------------------------------------- /packages/nextjs-routes/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tsconfig.json 3 | webpack.config.* 4 | -------------------------------------------------------------------------------- /packages/nextjs-routes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-routes", 3 | "version": "2.2.5", 4 | "description": "Type safe routing for Next.js", 5 | "license": "MIT", 6 | "author": "Tate ", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/tatethurston/nextjs-routes.git" 10 | }, 11 | "type": "module", 12 | "exports": { 13 | ".": { 14 | "require": "./dist/index.cjs", 15 | "default": "./dist/index.js" 16 | }, 17 | "./config": { 18 | "require": "./dist/config.cjs", 19 | "default": "./dist/config.js" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "bin": { 24 | "nextjs-routes": "dist/cli.js" 25 | }, 26 | "scripts": { 27 | "build": "tsc && webpack && chmod +x dist/cli.js", 28 | "clean": "rm -rf dist", 29 | "package:build": "pnpm run clean && pnpm run build && pnpm run package:copy:files && pnpm run package:prune", 30 | "package:copy:files": "cp ../../LICENSE ../../README.md .", 31 | "package:prune": "find dist -name *.test.* | xargs rm -f" 32 | }, 33 | "sideEffects": false, 34 | "dependencies": { 35 | "chokidar": "^4.0.3" 36 | }, 37 | "peerDependencies": { 38 | "next": "*" 39 | }, 40 | "keywords": [ 41 | "link", 42 | "next", 43 | "nextjs", 44 | "route", 45 | "routing", 46 | "type safe", 47 | "typescript" 48 | ], 49 | "typesVersions": { 50 | "*": { 51 | "*": [ 52 | "dist/*" 53 | ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import type { NextConfig } from "next"; 4 | import { writeNextJSRoutes } from "./core.js"; 5 | import { getAppDirectory, getPagesDirectory, isNotUndefined } from "./utils.js"; 6 | import { logger } from "./logger.js"; 7 | import { cwd } from "node:process"; 8 | import { existsSync } from "node:fs"; 9 | import { join } from "node:path"; 10 | import { pathToFileURL } from "url"; 11 | 12 | async function loadNextConfig(dir: string): Promise { 13 | const jsPath = join(dir, "next.config.js"); 14 | const mjsPath = join(dir, "next.config.mjs"); 15 | 16 | let path = ""; 17 | if (existsSync(jsPath)) { 18 | path = jsPath; 19 | } else if (existsSync(mjsPath)) { 20 | path = mjsPath; 21 | } 22 | 23 | if (!path) { 24 | return; 25 | } 26 | 27 | logger.info(`Found ${jsPath}`); 28 | const mod = (await import(pathToFileURL(path).href)).default; 29 | if (typeof mod == "function") { 30 | return await mod("phase-production-server", {}); 31 | } 32 | return mod; 33 | } 34 | 35 | async function cli(): Promise { 36 | const dir = cwd(); 37 | const config = await loadNextConfig(dir); 38 | if (!config) { 39 | logger.error( 40 | `Could not find a next.config.js or next.config.mjs. Expected to find either in ${dir}.`, 41 | ); 42 | process.exit(1); 43 | } 44 | 45 | const dirs = [ 46 | getPagesDirectory(process.cwd()), 47 | getAppDirectory(process.cwd()), 48 | ].filter(isNotUndefined); 49 | if (dirs.length === 0) { 50 | logger.error( 51 | `Could not find a pages or app directory. Expected to find eitherin ${dir}.`, 52 | ); 53 | 54 | process.exit(1); 55 | } 56 | 57 | writeNextJSRoutes(config); 58 | logger.info("Generated route types."); 59 | } 60 | 61 | await cli(); 62 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 4 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 5 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 6 | import { watch } from "chokidar"; 7 | import type { NextConfig } from "next"; 8 | import type { Configuration, WebpackPluginInstance } from "webpack"; 9 | import { NextJSRoutesOptions, writeNextJSRoutes } from "./core.js"; 10 | import { getAppDirectory, getPagesDirectory, isNotUndefined } from "./utils.js"; 11 | import { logger } from "./logger.js"; 12 | 13 | type WebpackConfigContext = Parameters>[1]; 14 | 15 | function debounce unknown>( 16 | fn: Fn, 17 | ms: number, 18 | ): (...args: Parameters) => void { 19 | let id: NodeJS.Timeout; 20 | return function (...args) { 21 | clearTimeout(id); 22 | id = setTimeout(() => fn(...args), ms); 23 | }; 24 | } 25 | 26 | interface NextJSRoutesPluginOptions extends WithRoutesOptions { 27 | /** 28 | * If you are getting the `Could not find a Next.js pages directory` error, 29 | * try passing `cwd: __dirname` from your `next.config.js`. 30 | */ 31 | cwd?: string; 32 | } 33 | 34 | class NextJSRoutesPlugin implements WebpackPluginInstance { 35 | name = "NextJSRoutesPlugin"; 36 | constructor( 37 | private readonly config: NextConfig, 38 | private readonly context: WebpackConfigContext, 39 | private readonly options: NextJSRoutesPluginOptions = {}, 40 | ) {} 41 | 42 | apply() { 43 | if (this.context.isServer) { 44 | return; 45 | } 46 | const opts = { 47 | cwd: process.cwd(), 48 | ...this.options, 49 | }; 50 | const watchDirs = [ 51 | getPagesDirectory(opts.cwd), 52 | getAppDirectory(opts.cwd), 53 | ].filter(isNotUndefined); 54 | 55 | if (watchDirs.length <= 0) { 56 | logger.error(`Could not find a Next.js pages directory. Expected to find either 'pages' (1), 'src/pages' (2), or 'app' (3) in your project root. 57 | 58 | 1. https://nextjs.org/docs/basic-features/pages 59 | 2. https://nextjs.org/docs/advanced-features/src-directory 60 | 3. https://beta.nextjs.org/docs/routing/fundamentals#the-app-directory 61 | `); 62 | return; 63 | } 64 | const options = { 65 | ...this.config, 66 | ...opts, 67 | }; 68 | if (this.context.dev) { 69 | const watcher = watch(watchDirs, { 70 | persistent: true, 71 | }); 72 | // batch changes 73 | const generate = debounce(() => writeNextJSRoutes(options), 50); 74 | watcher.on("add", generate).on("unlink", generate); 75 | } else { 76 | writeNextJSRoutes(options); 77 | } 78 | } 79 | } 80 | 81 | type WithRoutesOptions = Pick; 82 | 83 | export default function nextRoutes( 84 | options?: WithRoutesOptions, 85 | ): (nextConfig: NextConfig) => NextConfig { 86 | return function (nextConfig) { 87 | return { 88 | ...nextConfig, 89 | webpack: (config: Configuration, context) => { 90 | config.plugins ??= []; 91 | config.plugins.push( 92 | new NextJSRoutesPlugin(nextConfig, context, options), 93 | ); 94 | 95 | // invoke any existing webpack extensions 96 | if (nextConfig.webpack) { 97 | return nextConfig.webpack(config, context); 98 | } 99 | return config; 100 | }, 101 | }; 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/core.ts: -------------------------------------------------------------------------------- 1 | import type { I18NConfig } from "next/dist/server/config-shared.js"; 2 | import { existsSync, mkdirSync, writeFileSync } from "fs"; 3 | import { join, parse } from "path"; 4 | import { findFiles, getAppDirectory, getPagesDirectory } from "./utils.js"; 5 | import { version } from "./version.js"; 6 | 7 | type QueryType = "dynamic" | "catch-all" | "optional-catch-all"; 8 | 9 | interface Route { 10 | pathname: string; 11 | query: Record; 12 | } 13 | 14 | function convertWindowsPathToUnix(file: string): string { 15 | return file.replace(/\\/g, "/"); 16 | } 17 | 18 | export function nextRoutes(pathnames: string[]): Route[] { 19 | const DYNAMIC_SEGMENT_RE = /\[(.*?)\]/g; 20 | 21 | return pathnames.map((pathname) => { 22 | const segments: string[] = pathname.match(DYNAMIC_SEGMENT_RE) ?? []; 23 | const query = segments.reduce((acc, cur) => { 24 | const param = cur 25 | .replace(/\[/g, "") 26 | .replace(/\]/g, "") 27 | .replace("...", ""); 28 | let queryType: QueryType; 29 | if (cur.startsWith("[[...")) { 30 | queryType = "optional-catch-all"; 31 | } else if (cur.startsWith("[...")) { 32 | queryType = "catch-all"; 33 | } else { 34 | queryType = "dynamic"; 35 | } 36 | acc[param] = queryType; 37 | return acc; 38 | }, {}); 39 | 40 | return { 41 | pathname, 42 | query, 43 | }; 44 | }); 45 | } 46 | 47 | function getQueryInterface( 48 | query: Route["query"], 49 | ): [query: string, requiredKeys: number, optionalCatchAll: boolean] { 50 | let requiredKeys = 0; 51 | let optionalCatchAll = false; 52 | const keys = Object.entries(query) 53 | .map(([key, value]) => { 54 | switch (value) { 55 | case "dynamic": { 56 | requiredKeys += 1; 57 | return `"${key}": string`; 58 | } 59 | case "catch-all": { 60 | requiredKeys += 1; 61 | return `"${key}": string[]`; 62 | } 63 | case "optional-catch-all": { 64 | optionalCatchAll = true; 65 | return `"${key}"?: string[] | undefined`; 66 | } 67 | // istanbul ignore next: exhaustive typecheck 68 | default: { 69 | const _exhaust: never = value; 70 | return _exhaust; 71 | } 72 | } 73 | }) 74 | .join("; "); 75 | 76 | return [`{ ${keys} }`, requiredKeys, optionalCatchAll]; 77 | } 78 | 79 | interface GenerateConfig extends NextJSRoutesOptions { 80 | usingAppDirectory: boolean; 81 | usingPagesDirectory: boolean; 82 | } 83 | 84 | function generate(routes: Route[], config: GenerateConfig): string { 85 | const i18n = config.i18n ?? { 86 | defaultLocale: "", 87 | domains: [], 88 | localeDetection: false, 89 | locales: [], 90 | }; 91 | let output = `\ 92 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 93 | // This file will be automatically regenerated when your Next.js server is running. 94 | // nextjs-routes version: ${version} 95 | /* eslint-disable */ 96 | 97 | // prettier-ignore 98 | declare module "nextjs-routes" { 99 | import type { 100 | GetServerSidePropsContext as NextGetServerSidePropsContext, 101 | GetServerSidePropsResult as NextGetServerSidePropsResult 102 | } from "next"; 103 | 104 | export type Route = 105 | ${ 106 | !routes.length 107 | ? "never" 108 | : `| ${routes 109 | .sort((a, b) => a.pathname.localeCompare(b.pathname)) 110 | .map((route) => { 111 | const [params, requiredKeys, optionalCatchAll] = 112 | getQueryInterface(route.query); 113 | return requiredKeys > 0 || optionalCatchAll 114 | ? `DynamicRoute<"${route.pathname}", ${params}>` 115 | : `StaticRoute<"${route.pathname}">`; 116 | }) 117 | .join("\n | ")}` 118 | }; 119 | 120 | interface StaticRoute { 121 | pathname: Pathname; 122 | query?: Query | undefined; 123 | hash?: string | null | undefined; 124 | } 125 | 126 | interface DynamicRoute { 127 | pathname: Pathname; 128 | query: Parameters & Query; 129 | hash?: string | null | undefined; 130 | } 131 | 132 | interface Query { 133 | [key: string]: string | string[] | undefined; 134 | }; 135 | 136 | export type RoutedQuery

= Extract< 137 | Route, 138 | { pathname: P } 139 | >["query"]; 140 | 141 | export type Locale = ${ 142 | !i18n.locales.length 143 | ? "undefined" 144 | : `\n | ${i18n.locales.map((x) => `"${x}"`).join("\n | ")}` 145 | }; 146 | 147 | type Brand = K & { __brand: T }; 148 | 149 | /** 150 | * A string that is a valid application route. 151 | */ 152 | export type RouteLiteral = Brand 153 | 154 | /** 155 | * A typesafe utility function for generating paths in your application. 156 | * 157 | * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". 158 | */ 159 | export declare function route(r: Route): RouteLiteral; 160 | 161 | /** 162 | * Nearly identical to GetServerSidePropsContext from next, but further narrows 163 | * types based on nextjs-route's route data. 164 | */ 165 | export type GetServerSidePropsContext< 166 | Pathname extends Route["pathname"] = Route["pathname"], 167 | Preview extends NextGetServerSidePropsContext["previewData"] = NextGetServerSidePropsContext["previewData"] 168 | > = Omit & { 169 | params: Extract["query"]; 170 | query: Query; 171 | defaultLocale${ 172 | i18n.defaultLocale ? `: "${i18n.defaultLocale}"` : "?: undefined" 173 | }; 174 | locale${!i18n.locales.length ? "?:" : ":"} Locale; 175 | locales${ 176 | i18n.locales.length ? `: ${print(i18n.locales, 8)}` : "?: undefined" 177 | }; 178 | }; 179 | 180 | /** 181 | * Nearly identical to GetServerSideProps from next, but further narrows 182 | * types based on nextjs-route's route data. 183 | */ 184 | export type GetServerSideProps< 185 | Props extends { [key: string]: any } = { [key: string]: any }, 186 | Pathname extends Route["pathname"] = Route["pathname"], 187 | Preview extends NextGetServerSideProps["previewData"] = NextGetServerSideProps["previewData"] 188 | > = ( 189 | context: GetServerSidePropsContext 190 | ) => Promise> 191 | } 192 | 193 | // prettier-ignore 194 | declare module "next/link" { 195 | ${(() => { 196 | if (config.usingAppDirectory) { 197 | return 'import type { Route, RouteLiteral } from "nextjs-routes";'; 198 | } else { 199 | return 'import type { Route } from "nextjs-routes";'; 200 | } 201 | })()}; 202 | import type { LinkProps as NextLinkProps } from "next/dist/client/link"; 203 | import type React from "react"; 204 | 205 | type StaticRoute = Exclude["pathname"]; 206 | 207 | export type LinkProps = Omit & { 208 | href: ${(() => { 209 | if (config.usingPagesDirectory && config.usingAppDirectory) { 210 | return 'Route | StaticRoute | Omit | RouteLiteral'; 211 | } else if (config.usingPagesDirectory) { 212 | return 'Route | StaticRoute | Omit'; 213 | } else { 214 | return "StaticRoute | RouteLiteral"; 215 | } 216 | })()}; 217 | locale?: ${!i18n.locales.length ? "false" : `Locale | false`}; 218 | } 219 | 220 | /** 221 | * A React component that extends the HTML \`\` element to provide [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 222 | * and client-side navigation between routes. 223 | * 224 | * It is the primary way to navigate between routes in Next.js. 225 | * 226 | * Read more: [Next.js docs: \`\`](https://nextjs.org/docs/app/api-reference/components/link) 227 | */ 228 | declare const Link: React.ForwardRefExoticComponent, keyof LinkProps> & LinkProps & { 229 | children?: React.ReactNode; 230 | } & React.RefAttributes>; 231 | export default Link; 232 | } 233 | 234 | // prettier-ignore 235 | declare module "next/router" { 236 | import type { Locale, Route, RoutedQuery } from "nextjs-routes"; 237 | import type { NextRouter as Router } from "next/dist/client/router"; 238 | export * from "next/dist/client/router"; 239 | export { default } from "next/dist/client/router"; 240 | 241 | type NextTransitionOptions = NonNullable[2]>; 242 | type StaticRoute = Exclude["pathname"]; 243 | 244 | interface TransitionOptions extends Omit { 245 | locale?: ${!i18n.locales.length ? "false" : `Locale | false`}; 246 | } 247 | 248 | type PathnameAndQuery = Required< 249 | Pick, "pathname" | "query"> 250 | >; 251 | 252 | type AutomaticStaticOptimizedQuery = Omit & { 253 | query: Partial; 254 | }; 255 | 256 | type BaseRouter = 257 | | ({ isReady: false } & AutomaticStaticOptimizedQuery) 258 | | ({ isReady: true } & PaQ); 259 | 260 | export type NextRouter

= 261 | BaseRouter> & 262 | Omit< 263 | Router, 264 | | "defaultLocale" 265 | | "domainLocales" 266 | | "isReady" 267 | | "locale" 268 | | "locales" 269 | | "pathname" 270 | | "push" 271 | | "query" 272 | | "replace" 273 | | "route" 274 | > & { 275 | defaultLocale${ 276 | i18n.defaultLocale ? `: "${i18n.defaultLocale}"` : "?: undefined" 277 | }; 278 | domainLocales${ 279 | i18n.domains?.length ? `: ${print(i18n.domains, 8)}` : "?: undefined" 280 | }; 281 | locale${!i18n.locales.length ? "?:" : ":"} Locale; 282 | locales${ 283 | i18n.locales.length ? `: ${print(i18n.locales, 8)}` : "?: undefined" 284 | }; 285 | push( 286 | url: Route | StaticRoute | Omit, 287 | as?: string, 288 | options?: TransitionOptions 289 | ): Promise; 290 | replace( 291 | url: Route | StaticRoute | Omit, 292 | as?: string, 293 | options?: TransitionOptions 294 | ): Promise; 295 | route: P; 296 | }; 297 | 298 | export function useRouter

(): NextRouter

; 299 | } 300 | `; 301 | 302 | if (config.usingAppDirectory) { 303 | output += `\ 304 | 305 | // prettier-ignore 306 | declare module "next/navigation" { 307 | export * from "next/dist/client/components/navigation"; 308 | import type { Route, RouteLiteral, RoutedQuery } from "nextjs-routes"; 309 | import type { AppRouterInstance as NextAppRouterInstance, NavigateOptions, PrefetchOptions } from "next/dist/shared/lib/app-router-context.shared-runtime"; 310 | 311 | type StaticRoute = Exclude["pathname"]; 312 | 313 | /** 314 | * A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook 315 | * that lets you read the current URL's pathname. 316 | * 317 | * @example 318 | * \`\`\`ts 319 | * "use client" 320 | * import { usePathname } from 'next/navigation' 321 | * 322 | * export default function Page() { 323 | * const pathname = usePathname() // returns "/dashboard" on /dashboard?foo=bar 324 | * // ... 325 | * } 326 | * \`\`\` 327 | * 328 | * Read more: [Next.js Docs: \`usePathname\`](https://nextjs.org/docs/app/api-reference/functions/use-pathname) 329 | */ 330 | export const usePathname: () => RouteLiteral; 331 | 332 | type AppRouterInstance = Omit & { 333 | push(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void; 334 | replace(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void; 335 | prefetch(href: StaticRoute | RouteLiteral, options?: PrefetchOptions): void; 336 | } 337 | 338 | /** 339 | * 340 | * This hook allows you to programmatically change routes inside [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components). 341 | * 342 | * @example 343 | * \`\`\`ts 344 | * "use client" 345 | * import { useRouter } from 'next/navigation' 346 | * 347 | * export default function Page() { 348 | * const router = useRouter() 349 | * // ... 350 | * router.push('/dashboard') // Navigate to /dashboard 351 | * } 352 | * \`\`\` 353 | * 354 | * Read more: [Next.js Docs: \`useRouter\`](https://nextjs.org/docs/app/api-reference/functions/use-router) 355 | */ 356 | export function useRouter(): AppRouterInstance; 357 | 358 | /** 359 | * A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook 360 | * that lets you read a route's dynamic params filled in by the current URL. 361 | * 362 | * @example 363 | * \`\`\`ts 364 | * "use client" 365 | * import { useParams } from 'next/navigation' 366 | * 367 | * export default function Page() { 368 | * // on /dashboard/[team] where pathname is /dashboard/nextjs 369 | * const { team } = useParams() // team === "nextjs" 370 | * } 371 | * \`\`\` 372 | * 373 | * Read more: [Next.js Docs: \`useParams\`](https://nextjs.org/docs/app/api-reference/functions/use-params) 374 | */ 375 | export const useParams: () => RoutedQuery; 376 | } 377 | `; 378 | } 379 | return output; 380 | } 381 | 382 | function print(x: unknown, indent: number): string { 383 | return JSON.stringify(x, undefined, 2) 384 | .split("\n") 385 | .join("\n" + " ".repeat(indent)); 386 | } 387 | 388 | export interface NextJSRoutesOptions { 389 | /** 390 | * Location of the Next.js project. Defaults to the current working directory. 391 | * 392 | * This is only necessary when working with a non standard NextJS project setup, such as Nx. 393 | * 394 | * Example: 395 | * 396 | * // next.config.js 397 | * const nextRoutes = require("nextjs-routes/config") 398 | * const withRoutes = nextRoutes({ dir: __dirname }); 399 | */ 400 | dir?: string | undefined; 401 | /** 402 | * The file path indicating the output directory where the generated route types 403 | * should be written to (e.g.: "types"). 404 | */ 405 | outDir?: string | undefined; 406 | /** 407 | * NextJS config option. 408 | * https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions 409 | */ 410 | pageExtensions?: string[] | undefined; 411 | /** 412 | * Internationalization configuration 413 | * 414 | * @see [Internationalization docs](https://nextjs.org/docs/advanced-features/i18n-routing) 415 | */ 416 | i18n?: I18NConfig | null | undefined; 417 | } 418 | 419 | interface Opts { 420 | pageExtensions: string[]; 421 | directory: string; 422 | } 423 | 424 | function commonProcessing(paths: string[], opts: Opts): string[] { 425 | return ( 426 | paths 427 | // filter page extensions 428 | .filter((file) => { 429 | return opts.pageExtensions.some((ext) => file.endsWith(ext)); 430 | }) 431 | // remove file extensions (.tsx, .test.tsx) 432 | .map((file) => file.replace(/(\.\w+)+$/, "")) 433 | // remove duplicates from file extension removal (eg foo.ts and foo.test.ts) 434 | .filter((file, idx, array) => array.indexOf(file) === idx) 435 | // remove page directory path 436 | .map((file) => file.replace(opts.directory, "")) 437 | // normalize paths from windows users 438 | .map(convertWindowsPathToUnix) 439 | ); 440 | } 441 | 442 | const APP_DIRECTORY_ROUTABLE_DIRECTORIES = ["page", "route"]; 443 | 444 | const APP_INTERCEPTING_ROUTE = ["(.)", "(..)", "(..)(..)", "(...)"]; 445 | 446 | function appDirectoryRoutable(file: string): boolean { 447 | const name = parse(file).name; 448 | return ( 449 | // only consider page and route 450 | APP_DIRECTORY_ROUTABLE_DIRECTORIES.includes(name) && 451 | // remove any filepaths that contain intercepts 452 | !APP_INTERCEPTING_ROUTE.some((intercept) => file.includes(intercept)) 453 | ); 454 | } 455 | 456 | export function getAppRoutes(files: string[], opts: Opts): string[] { 457 | return ( 458 | commonProcessing(files, opts) 459 | .filter(appDirectoryRoutable) 460 | .map((file) => 461 | // transform filepath to url path 462 | file 463 | .split("/") 464 | // remove named groups 465 | .filter( 466 | (segment) => !(segment.startsWith("(") && segment.endsWith(")")), 467 | ) 468 | // remove page + route from path 469 | .filter( 470 | (segment) => 471 | !APP_DIRECTORY_ROUTABLE_DIRECTORIES.includes(parse(segment).name), 472 | ) 473 | // remove slots 474 | .filter((segment) => !segment.startsWith("@")) 475 | .join("/"), 476 | ) 477 | // handle index page 478 | .map((file) => (file === "" ? "/" : file)) 479 | ); 480 | } 481 | 482 | const NEXTJS_NON_ROUTABLE = ["/_app", "/_document", "/_error", "/middleware"]; 483 | 484 | export function getPageRoutes(files: string[], opts: Opts): string[] { 485 | return ( 486 | commonProcessing(files, opts) 487 | // remove index if present (/foos/index.ts is the same as /foos.ts) 488 | .map((file) => file.replace(/index$/, "")) 489 | // remove trailing slash if present 490 | .map((file) => 491 | file.endsWith("/") && file.length > 2 ? file.slice(0, -1) : file, 492 | ) 493 | // exclude nextjs special routes 494 | .filter((file) => !NEXTJS_NON_ROUTABLE.includes(file)) 495 | ); 496 | } 497 | 498 | export function writeNextJSRoutes(options: NextJSRoutesOptions): void { 499 | const defaultOptions = { 500 | usingPagesDirectory: false, 501 | usingAppDirectory: false, 502 | dir: process.cwd(), 503 | outDir: join(options.dir ?? process.cwd(), "@types"), 504 | pageExtensions: ["tsx", "ts", "jsx", "js"], 505 | }; 506 | const opts = { 507 | ...defaultOptions, 508 | ...options, 509 | }; 510 | const files = []; 511 | const pagesDirectory = getPagesDirectory(opts.dir); 512 | if (pagesDirectory) { 513 | const routes = getPageRoutes(findFiles(pagesDirectory), { 514 | pageExtensions: opts.pageExtensions, 515 | directory: pagesDirectory, 516 | }); 517 | files.push(...routes); 518 | opts.usingPagesDirectory = true; 519 | } 520 | const appDirectory = getAppDirectory(opts.dir); 521 | if (appDirectory) { 522 | const routes = getAppRoutes(findFiles(appDirectory), { 523 | pageExtensions: opts.pageExtensions, 524 | directory: appDirectory, 525 | }); 526 | files.push(...routes); 527 | opts.usingAppDirectory = true; 528 | } 529 | const outputFilepath = join(opts.outDir, "nextjs-routes.d.ts"); 530 | if (opts.outDir && !existsSync(opts.outDir)) { 531 | mkdirSync(opts.outDir, { recursive: true }); 532 | } 533 | const routes = nextRoutes(files); 534 | const generated = generate(routes, opts); 535 | writeFileSync(outputFilepath, generated); 536 | } 537 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/index.ts: -------------------------------------------------------------------------------- 1 | interface Route { 2 | pathname: string; 3 | query?: { [key: string]: string | string[] | undefined }; 4 | hash?: string | null | undefined; 5 | } 6 | 7 | interface OptionalCatchAll extends Route { 8 | query?: { [key: string]: string[] | undefined }; 9 | } 10 | 11 | interface CatchAll extends Route { 12 | query: { [key: string]: string[] }; 13 | } 14 | 15 | interface Dynamic extends Route { 16 | query: { [key: string]: string }; 17 | } 18 | 19 | interface RouteOptions { 20 | trailingSlash?: boolean; 21 | } 22 | 23 | export function route( 24 | r: Route, 25 | options: RouteOptions = { trailingSlash: false }, 26 | ): string { 27 | const params = new Set(); 28 | let path = 29 | "/" + 30 | r.pathname 31 | .split("/") 32 | .map((segment) => { 33 | // optional catch all 34 | if (segment.startsWith("[[...") && segment.endsWith("]]")) { 35 | const query = segment.slice(5, -2); 36 | params.add(query); 37 | return (r as OptionalCatchAll).query?.[query]?.join("/"); 38 | } 39 | // catch all 40 | if (segment.startsWith("[...") && segment.endsWith("]")) { 41 | const query = segment.slice(4, -1); 42 | params.add(query); 43 | return (r as CatchAll).query[query].join("/"); 44 | } 45 | // dynamic 46 | if (segment.startsWith("[") && segment.endsWith("]")) { 47 | const query = segment.slice(1, -1); 48 | params.add(query); 49 | return (r as Dynamic).query[query]; 50 | } 51 | return segment; 52 | }) 53 | // removes optional catch all if no query is supplied 54 | .filter(Boolean) 55 | .join("/"); 56 | 57 | if (options.trailingSlash) { 58 | path += "/"; 59 | } 60 | 61 | const search = new URLSearchParams(); 62 | for (const key in r.query) { 63 | if (!params.has(key)) { 64 | const value = r.query[key]; 65 | if (Array.isArray(value)) { 66 | value.forEach((val) => search.append(key, val)); 67 | } else if (value !== undefined) { 68 | search.append(key, value); 69 | } 70 | } 71 | } 72 | const qs = search.toString().length > 0 ? "?" + search.toString() : ""; 73 | const hash = r.hash ? "#" + r.hash : ""; 74 | 75 | return path + qs + hash; 76 | } 77 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/logger.ts: -------------------------------------------------------------------------------- 1 | export const logger: Pick = { 2 | error: (str: string) => console.error("[nextjs-routes] " + str), 3 | info: (str: string) => console.info("[nextjs-routes] " + str), 4 | }; 5 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/test/core.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, writeFileSync } from "fs"; 2 | import { writeNextJSRoutes } from "../core.js"; 3 | import { findFiles } from "../utils.js"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 6 | jest.mock("fs", () => ({ 7 | ...jest.requireActual("fs"), 8 | writeFileSync: jest.fn(), 9 | existsSync: jest.fn(() => false), 10 | mkdirSync: jest.fn(), 11 | })); 12 | const writeFileSyncMock = writeFileSync as jest.Mock; 13 | const existsSyncMock = existsSync as jest.Mock; 14 | jest.spyOn(process, "cwd").mockReturnValue(""); 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 17 | jest.mock("../utils.js", () => ({ 18 | ...jest.requireActual("../utils.js"), 19 | findFiles: jest.fn(), 20 | })); 21 | const findFilesMock = findFiles as jest.Mock; 22 | 23 | describe("route generation", () => { 24 | it("no routes", () => { 25 | // getPageRoutes 26 | existsSyncMock.mockImplementationOnce(() => true); 27 | findFilesMock.mockReturnValueOnce([]); 28 | writeNextJSRoutes({}); 29 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 30 | }); 31 | 32 | it("transforms windows paths", () => { 33 | // getPageRoutes src/pages 34 | existsSyncMock 35 | .mockImplementationOnce(() => false) 36 | .mockImplementationOnce(() => true); 37 | findFilesMock.mockReturnValueOnce(["src\\pages\\[foo]\\bar\\index.ts"]); 38 | writeNextJSRoutes({}); 39 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 40 | }); 41 | 42 | it("dedupes", () => { 43 | // getPageRoutes 44 | existsSyncMock.mockImplementationOnce(() => true); 45 | findFilesMock.mockReturnValueOnce(["pages/index.tsx", "pages/index.ts"]); 46 | writeNextJSRoutes({}); 47 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 48 | }); 49 | 50 | it("typescript", () => { 51 | // getPageRoutes 52 | existsSyncMock.mockImplementationOnce(() => true); 53 | findFilesMock.mockReturnValueOnce([ 54 | "pages/404.ts", 55 | "pages/[foo].ts", 56 | "pages/[foo]/[bar]/[baz].ts", 57 | "pages/[foo]/bar/[baz].ts", 58 | "pages/[foo]/bar/[baz]/foo/[bar].ts", 59 | "pages/[foo]/baz.ts", 60 | "pages/_app.ts", 61 | "pages/middleware.ts", 62 | "pages/_debug.ts", 63 | "pages/_debug/health-check.ts", 64 | "pages/_document.ts", 65 | "pages/_error.ts", 66 | "pages/_error/index.ts", 67 | "pages/api/[[...segments]].ts", 68 | "pages/api/[...segments].ts", 69 | "pages/api/bar.ts", 70 | "pages/foo/[slug].ts", 71 | "pages/index.ts", 72 | "pages/not-found.ts", 73 | "pages/settings/bars/[bar].ts", 74 | "pages/settings/bars/[bar]/baz.ts", 75 | "pages/settings/foo.ts", 76 | "pages/settings/index.ts", 77 | ]); 78 | writeNextJSRoutes({}); 79 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 80 | }); 81 | 82 | describe("app directory", () => { 83 | it("generates routes", () => { 84 | // getAppRoutes 85 | existsSyncMock 86 | .mockImplementationOnce(() => false) 87 | .mockImplementationOnce(() => false) 88 | .mockImplementationOnce(() => true); 89 | findFilesMock.mockReturnValueOnce([ 90 | "app/page.ts", 91 | "app/bar/page.ts", 92 | "app/foo/page.tsx", 93 | "app/foobar/page.js", 94 | "app/barbaz/page.jsx", 95 | "app/baz/route.js", 96 | "app/foobaz/route.ts", 97 | ]); 98 | writeNextJSRoutes({}); 99 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 100 | }); 101 | 102 | it("handles intercept routes", () => { 103 | // getAppRoutes 104 | existsSyncMock 105 | .mockImplementationOnce(() => false) 106 | .mockImplementationOnce(() => false) 107 | .mockImplementationOnce(() => true); 108 | findFilesMock.mockReturnValueOnce([ 109 | "app/(..)photo/[id]/page.ts", 110 | "app/photo/[id]/page.ts", 111 | "app/foo/(...)bar/page.ts", 112 | "app/foobar/(.)baz/page.ts", 113 | "app/foobar/(..)baz/page.ts", 114 | ]); 115 | writeNextJSRoutes({}); 116 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 117 | }); 118 | 119 | it("handles parallel routes", () => { 120 | // getAppRoutes 121 | existsSyncMock 122 | .mockImplementationOnce(() => false) 123 | .mockImplementationOnce(() => false) 124 | .mockImplementationOnce(() => true); 125 | findFilesMock.mockReturnValueOnce([ 126 | "app/@team/settings/page.ts", 127 | "app/@analytics/page.ts", 128 | ]); 129 | writeNextJSRoutes({}); 130 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 131 | }); 132 | 133 | it("handles windows paths", () => { 134 | // get AppRoutes 135 | existsSyncMock 136 | .mockImplementationOnce(() => false) 137 | .mockImplementationOnce(() => false) 138 | .mockImplementationOnce(() => true); 139 | findFilesMock.mockReturnValueOnce(["app\\[foo]\\bar\\page.ts"]); 140 | writeNextJSRoutes({}); 141 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 142 | }); 143 | }); 144 | 145 | describe("pages and app directory", () => { 146 | it("generates routes", () => { 147 | existsSyncMock 148 | // getPageRoutes 149 | .mockImplementationOnce(() => true) 150 | // getAppRoutes 151 | .mockImplementationOnce(() => true); 152 | findFilesMock 153 | // page routes 154 | .mockReturnValueOnce(["pages/[foo].ts"]) 155 | // app routes 156 | .mockReturnValueOnce(["app/bar/page.ts", "app/page.ts"]); 157 | writeNextJSRoutes({}); 158 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 159 | }); 160 | }); 161 | 162 | describe("configuration", () => { 163 | describe("pageExtensions", () => { 164 | it("default", () => { 165 | // getPageRoutes 166 | existsSyncMock.mockImplementationOnce(() => true); 167 | findFilesMock.mockReturnValueOnce(["pages/404.ts", "pages/404.md"]); 168 | writeNextJSRoutes({}); 169 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 170 | }); 171 | 172 | it("configured", () => { 173 | // getPageRoutes 174 | existsSyncMock.mockImplementationOnce(() => true); 175 | findFilesMock.mockReturnValueOnce([ 176 | "pages/404.ts", 177 | "pages/index.md", 178 | "pages/foo/index.page.tsx", 179 | "pages/foo/index.test.tsx", 180 | ]); 181 | writeNextJSRoutes({ 182 | pageExtensions: ["ts", "md", "page.tsx"], 183 | }); 184 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 185 | }); 186 | }); 187 | 188 | it("outDir", () => { 189 | // getPageRoutes 190 | existsSyncMock.mockImplementationOnce(() => true); 191 | findFilesMock.mockReturnValueOnce(["pages/404.ts"]); 192 | writeNextJSRoutes({ outDir: "src" }); 193 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 194 | }); 195 | 196 | it("i18n", () => { 197 | // getPageRoutes 198 | existsSyncMock.mockImplementationOnce(() => true); 199 | findFilesMock.mockReturnValueOnce(["pages/index.ts"]); 200 | writeNextJSRoutes({ 201 | i18n: { 202 | locales: ["en-US", "fr", "nl-NL"], 203 | defaultLocale: "en-US", 204 | domains: [ 205 | { 206 | domain: "example.nl", 207 | defaultLocale: "nl-NL", 208 | locales: ["nl-BE"], 209 | }, 210 | { 211 | domain: "example.fr", 212 | defaultLocale: "fr", 213 | http: true, 214 | }, 215 | ], 216 | }, 217 | }); 218 | expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); 219 | }); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { route } from "../index.js"; 2 | 3 | describe(route, () => { 4 | describe("generates paths", () => { 5 | // prettier-ignore 6 | test.each([ 7 | [{ pathname: "/404" }, "/404"], 8 | [{ pathname: "/settings/about" }, "/settings/about"], 9 | // dynamic 10 | [{ pathname: "/foos/[foo]", query: { foo: "bar" } }, "/foos/bar"], 11 | [{ pathname: "/foos/[foo]/bars/[bar]", query: { foo: "bar", bar: "baz" } }, "/foos/bar/bars/baz"], 12 | [{ pathname: "/[foo]/[bar]/[baz]", query: { foo: "foo", bar: "bar", baz: "baz" } }, "/foo/bar/baz"], 13 | // catch all 14 | [{ pathname: "/[...segments]", query: { segments: ["foo"] } }, "/foo"], 15 | [{ pathname: "/[...segments]", query: { segments: ["foo", "bar"] } }, "/foo/bar"], 16 | // optional catch all 17 | [{ pathname: "/[[...segments]]", query: { segments: [] } }, "/"], 18 | [{ pathname: "/[[...segments]]", query: { segments: undefined } }, "/"], 19 | [{ pathname: "/[[...segments]]/foos", query: { segments: undefined } }, "/foos"], 20 | // query params 21 | [{ pathname: "/foos/[foo]", query: { foo: "foo", bar: "bar" } }, "/foos/foo?bar=bar"], 22 | [{ pathname: "/foos/[foo]", query: { foo: "foo", bar: "bar", baz: ["1", "2", "3"] } }, "/foos/foo?bar=bar&baz=1&baz=2&baz=3"], 23 | [{ pathname: "/foos/[foo]", query: { foo: "foo", bar: "bar", baz: ["1", "2", "3"] }, hash: "foo" }, "/foos/foo?bar=bar&baz=1&baz=2&baz=3#foo"], 24 | [{ pathname: "/foos/[foo]", query: { foo: "foo", bar: undefined, baz: '', foobar: '' } }, "/foos/foo?baz=&foobar="], 25 | ])("generates paths for %o", (input, expected) => { 26 | expect(route(input)).toEqual(expected); 27 | }); 28 | }); 29 | 30 | describe("options", () => { 31 | describe("trailingSlash", () => { 32 | describe.each([ 33 | ["/settings/about", undefined, true, "/settings/about/"], 34 | ["/settings/about", undefined, false, "/settings/about"], 35 | ["/foos/[foo]", { foo: "bar" }, true, "/foos/bar/"], 36 | ["/foos/[foo]", { foo: "bar" }, false, "/foos/bar"], 37 | ])( 38 | "route(%p, { trailingSlash: %p })", 39 | (pathname, query, trailingSlash, expectedResult) => { 40 | it(`returns ${expectedResult}`, () => { 41 | expect(route({ pathname, query }, { trailingSlash })).toEqual( 42 | expectedResult, 43 | ); 44 | }); 45 | }, 46 | ); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/test/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../logger.js"; 2 | 3 | describe("logger", () => { 4 | describe("#error", () => { 5 | it("prefixes with [nextjs-routes]", () => { 6 | const consoleError = jest 7 | .spyOn(console, "error") 8 | .mockImplementation(() => {}); 9 | 10 | logger.error("Some error"); 11 | expect(consoleError).toHaveBeenCalledWith("[nextjs-routes] Some error"); 12 | }); 13 | }); 14 | 15 | describe("#info", () => { 16 | it("prefixes with [nextjs-routes]", () => { 17 | const consoleInfo = jest 18 | .spyOn(console, "info") 19 | .mockImplementation(() => {}); 20 | 21 | logger.info("Some info"); 22 | expect(consoleInfo).toHaveBeenCalledWith("[nextjs-routes] Some info"); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getPagesDirectory, 3 | getAppDirectory, 4 | findFiles, 5 | isNotUndefined, 6 | } from "../utils.js"; 7 | import mockFS from "mock-fs"; 8 | 9 | describe(getPagesDirectory, () => { 10 | afterEach(() => { 11 | mockFS.restore(); 12 | }); 13 | 14 | it("finds pages in top level of directory", () => { 15 | mockFS({ 16 | "/my-dir": { 17 | pages: { 18 | "file1.js": "content1", 19 | }, 20 | }, 21 | }); 22 | 23 | expect(getPagesDirectory("/my-dir")).toEqual("/my-dir/pages"); 24 | }); 25 | 26 | it("finds pages in src directory", () => { 27 | mockFS({ 28 | "/my-dir": { 29 | src: { 30 | pages: { 31 | "file1.js": "content1", 32 | }, 33 | }, 34 | }, 35 | }); 36 | 37 | expect(getPagesDirectory("/my-dir")).toEqual("/my-dir/src/pages"); 38 | }); 39 | }); 40 | 41 | describe(getAppDirectory, () => { 42 | afterEach(() => { 43 | mockFS.restore(); 44 | }); 45 | 46 | it("finds app in top level of directory", () => { 47 | mockFS({ 48 | "/my-dir": { 49 | app: { 50 | "file1.js": "content1", 51 | }, 52 | }, 53 | }); 54 | 55 | expect(getAppDirectory("/my-dir")).toEqual("/my-dir/app"); 56 | }); 57 | 58 | it("finds app in src directory", () => { 59 | mockFS({ 60 | "/my-dir": { 61 | src: { 62 | app: { 63 | "file1.js": "content1", 64 | }, 65 | }, 66 | }, 67 | }); 68 | 69 | expect(getAppDirectory("/my-dir")).toEqual("/my-dir/src/app"); 70 | }); 71 | }); 72 | 73 | describe(findFiles, () => { 74 | afterEach(() => { 75 | mockFS.restore(); 76 | }); 77 | 78 | it("return a list of files in a directory", () => { 79 | mockFS({ 80 | "/my-dir": { 81 | "file1.js": "content1", 82 | "file2.js": "content2", 83 | "sub-dir": { 84 | "file3.js": "content3", 85 | }, 86 | }, 87 | }); 88 | 89 | expect(findFiles("/my-dir")).toEqual([ 90 | "/my-dir/file1.js", 91 | "/my-dir/file2.js", 92 | "/my-dir/sub-dir/file3.js", 93 | ]); 94 | }); 95 | 96 | it("ignores node_modules", () => { 97 | mockFS({ 98 | "/my-dir": { 99 | "file1.js": "content1", 100 | node_modules: { 101 | "file2.js": "content2", 102 | }, 103 | }, 104 | }); 105 | 106 | expect(findFiles("/my-dir")).toEqual(["/my-dir/file1.js"]); 107 | }); 108 | }); 109 | 110 | describe(isNotUndefined, () => { 111 | it("true when not undefined", () => { 112 | expect(isNotUndefined("hello")).toBe(true); 113 | }); 114 | 115 | it("false when undefined", () => { 116 | expect(isNotUndefined(undefined)).toBe(false); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readdirSync, statSync } from "fs"; 2 | import { join } from "path"; 3 | 4 | function findDir(cwd: string, dir: string): string | undefined { 5 | const paths = [join(cwd, dir), join(cwd, "src", dir)]; 6 | for (const path of paths) { 7 | if (existsSync(path)) { 8 | return path; 9 | } 10 | } 11 | } 12 | 13 | export function getPagesDirectory(cwd: string): string | undefined { 14 | return findDir(cwd, "pages"); 15 | } 16 | 17 | export function getAppDirectory(cwd: string): string | undefined { 18 | return findDir(cwd, "app"); 19 | } 20 | 21 | export function findFiles(entry: string): string[] { 22 | return readdirSync(entry).flatMap((file) => { 23 | const filepath = join(entry, file); 24 | if (filepath.includes("node_modules")) { 25 | return []; 26 | } 27 | if (statSync(filepath).isDirectory()) { 28 | return findFiles(filepath); 29 | } 30 | return filepath; 31 | }); 32 | } 33 | 34 | export function isNotUndefined(value: T | undefined): value is T { 35 | return value !== undefined; 36 | } 37 | -------------------------------------------------------------------------------- /packages/nextjs-routes/src/version.ts: -------------------------------------------------------------------------------- 1 | export const version = "2.2.5"; 2 | -------------------------------------------------------------------------------- /packages/nextjs-routes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/nextjs-routes/webpack.config.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | const { join } = require("path"); 3 | 4 | /** 5 | * @type {import('webpack').Configuration} 6 | */ 7 | const common = { 8 | mode: "production", 9 | resolve: { 10 | extensionAlias: { 11 | ".js": [".ts", ".js"], 12 | }, 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(m|c)?tsx?$/, 18 | exclude: /node_modules/, 19 | use: { 20 | loader: "swc-loader", 21 | options: { 22 | jsc: { 23 | target: "es2020", 24 | parser: { 25 | syntax: "typescript", 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | }; 34 | 35 | module.exports = [ 36 | { 37 | ...common, 38 | target: "node", 39 | entry: { 40 | index: "./src/config.ts", 41 | }, 42 | output: { 43 | filename: "config.cjs", 44 | // eslint-disable-next-line no-undef 45 | path: join(__dirname, "dist"), 46 | library: { 47 | type: "commonjs2", 48 | export: "default", 49 | }, 50 | }, 51 | externals: { 52 | chokidar: "chokidar", 53 | }, 54 | externalsPresets: { 55 | node: true, 56 | }, 57 | }, 58 | { 59 | ...common, 60 | target: "node", 61 | entry: { 62 | index: "./src/index.ts", 63 | }, 64 | output: { 65 | filename: "index.cjs", 66 | // eslint-disable-next-line no-undef 67 | path: join(__dirname, "dist"), 68 | libraryTarget: "commonjs2", 69 | }, 70 | }, 71 | ]; 72 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "module": "esnext", 9 | "moduleResolution": "Node", 10 | "noEmitOnError": false, 11 | "outDir": "dist", 12 | "resolveJsonModule": true, 13 | "rootDir": "src", 14 | "skipLibCheck": true, 15 | "sourceMap": false, 16 | "strict": true, 17 | "target": "ES2020" 18 | } 19 | } 20 | --------------------------------------------------------------------------------