Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for experimental.typedRoutes #15

Merged
merged 12 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fuzzy-parrots-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"next-safe-navigation": minor
---

Add support for `experimental.typedRoutes`

You may now enable `experimental.typedRoutes` in `next.config.js` to have a better and safer experience with autocomplete when defining your routes
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,11 @@ jobs:

- name: ♻️ Cache node_modules
uses: actions/cache@v4
id: bun-cache
id: cache
with:
path: "**/node_modules"
key: ${{ runner.os }}-node-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-node-

if: steps.bun-cache.outputs.cache-hit != 'true'
- run: bun install --frozen-lockfile

lint-package:
Expand Down Expand Up @@ -62,7 +59,7 @@ jobs:
key: ${{ matrix.os }}-eslint-${{ hashFiles('**/*.ts', 'package.json', 'tsconfig.json') }}

- name: 📦 Install dependencies
if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile

- name: 🚨 Lint files
Expand All @@ -88,7 +85,7 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/bun.lockb') }}

- name: 📦 Install dependencies
if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile

- name: 🧪 Run tests
Expand Down Expand Up @@ -119,7 +116,7 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/bun.lockb') }}

- name: 📦 Install dependencies
if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile

- name: 🏗️ Build package
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ jobs:

- name: ♻️ Cache node_modules
uses: actions/cache@v4
id: bun-cache
id: cache
with:
path: "**/node_modules"
key: ${{ runner.os }}-node-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-node-

if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
- run: bun install --frozen-lockfile

build:
Expand All @@ -54,7 +54,7 @@ jobs:
bun-version: latest

- name: 📦 Install dependencies
if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile

- name: 🚨 Check for errors
Expand Down Expand Up @@ -159,7 +159,7 @@ jobs:
registry-url: 'https://npm.pkg.github.com'

- name: 📦 Install dependencies
if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile

- name: 🏷️ Overwrite package name with user scope
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ jobs:

- name: ♻️ Cache node_modules
uses: actions/cache@v4
id: bun-cache
id: cache
with:
path: "**/node_modules"
key: ${{ runner.os }}-node-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-node-

if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
- run: bun install --frozen-lockfile

tests:
Expand All @@ -50,7 +50,7 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/bun.lockb') }}

- name: 📦 Install dependencies
if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile

- name: 🧪 Run tests
Expand Down Expand Up @@ -81,7 +81,7 @@ jobs:
key: ${{ runner.os }}-node-${{ hashFiles('**/bun.lockb') }}

- name: 📦 Install dependencies
if: steps.bun-cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-hit != 'true'
run: bun install --frozen-lockfile

- name: 🏗️ Build package
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ npm install next-safe-navigation

## ⚡ Quick start

> [!WARNING]
> Ensure `experimental.typedRoutes` is disabled in `next.config.js`
> [!TIP]
> Enable `experimental.typedRoutes` in `next.config.js` for a better and safer experience with autocomplete when defining your routes

### Declare your application routes and parameters in a single place
```ts
Expand Down
Binary file modified bun.lockb
Binary file not shown.
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.1",
"@lukemorales/prettier-config": "^1.1.0",
"@testing-library/react": "^14.2.0",
"@testing-library/react": "^14.2.2",
"@types/bun": "latest",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.2.2",
"@vitest/ui": "^1.2.2",
"eslint-config-lukemorales": "^0.3.0",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.4.0",
"eslint-config-lukemorales": "^0.4.1",
"jsdom": "^24.0.0",
"next": "^14.1.0",
"next": "^14.1.4",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.4",
"tsup": "^8.0.1",
"prettier": "^3.2.5",
"tsup": "^8.0.2",
"typescript": "^4.8.2",
"vitest": "^1.2.2",
"vitest": "^1.4.0",
"zod": "^3.22.4"
},
"peerDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/convert-url-search-params-to-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function convertURLSearchParamsToObject(
const values = params.getAll(key);

acc[key] = values.length > 1 ? values : value;

return acc;
},
{},
Expand Down
69 changes: 40 additions & 29 deletions src/create-navigation-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,67 +10,77 @@ import { makeRouteBuilder, type RouteBuilder } from './make-route-builder';
import type { Prettify } from './types';

type AnyRouteBuilder =
| RouteBuilder<any, any>
| RouteBuilder<any, never>
| RouteBuilder<never, any>
| RouteBuilder<never, never>;
| RouteBuilder<string, any, any>
| RouteBuilder<string, any, never>
| RouteBuilder<string, never, any>
| RouteBuilder<string, never, never>;

type NavigationConfig = Record<string, AnyRouteBuilder>;

type SafeRootRoute = () => string;
type SafeRootRoute<Path extends string> = () => Path;

type SafeRouteWithParams<Params extends z.ZodSchema> = {
(options: z.input<Params>): string;
type SafeRouteWithParams<Path extends string, Params extends z.ZodSchema> = {
(options: z.input<Params>): Path;
$parseParams: (params: unknown) => z.output<Params>;
};

type SafeRouteWithSearch<Search extends z.ZodSchema> = {
(options?: { search?: z.input<Search> }): string;
type SafeRouteWithSearch<Path extends string, Search extends z.ZodSchema> = {
(options?: { search?: z.input<Search> }): Path;
$parseSearchParams: (searchParams: unknown) => z.output<Search>;
};

type SafeRouteWithRequiredSearch<Search extends z.ZodSchema> = {
(options: { search: z.input<Search> }): string;
type SafeRouteWithRequiredSearch<
Path extends string,
Search extends z.ZodSchema,
> = {
(options: { search: z.input<Search> }): Path;
$parseSearchParams: (searchParams: unknown) => z.output<Search>;
};

type SafeRouteWithParamsAndSearch<
Path extends string,
Params extends z.ZodSchema,
Search extends z.ZodSchema,
Options = z.input<Params> & { search?: z.input<Search> },
> = {
(options: Prettify<Options>): string;
(options: Prettify<Options>): Path;
$parseParams: (params: unknown) => z.output<Params>;
$parseSearchParams: (searchParams: unknown) => z.output<Search>;
};

type SafeRouteWithParamsAndRequiredSearch<
Path extends string,
Params extends z.ZodSchema,
Search extends z.ZodSchema,
Options = z.input<Params> & { search: z.input<Search> },
> = {
(options: Prettify<Options>): string;
(options: Prettify<Options>): Path;
$parseParams: (params: unknown) => z.output<Params>;
$parseSearchParams: (searchParams: unknown) => z.output<Search>;
};

type SafeRoute<Params extends z.ZodSchema, Search extends z.ZodSchema> =
[Params, Search] extends [never, never] ? SafeRootRoute
: [Params, Search] extends [z.ZodSchema, never] ? SafeRouteWithParams<Params>
type SafeRoute<
Path extends string,
Params extends z.ZodSchema,
Search extends z.ZodSchema,
> =
[Params, Search] extends [never, never] ? SafeRootRoute<Path>
: [Params, Search] extends [z.ZodSchema, never] ?
SafeRouteWithParams<Path, Params>
: [Params, Search] extends [never, z.ZodSchema] ?
undefined extends z.input<Search> ?
SafeRouteWithSearch<Search>
: SafeRouteWithRequiredSearch<Search>
SafeRouteWithSearch<Path, Search>
: SafeRouteWithRequiredSearch<Path, Search>
: [Params, Search] extends [z.ZodSchema, z.ZodSchema] ?
undefined extends z.input<Search> ?
SafeRouteWithParamsAndSearch<Params, Search>
: SafeRouteWithParamsAndRequiredSearch<Params, Search>
SafeRouteWithParamsAndSearch<Path, Params, Search>
: SafeRouteWithParamsAndRequiredSearch<Path, Params, Search>
: never;

type RouteWithParams<Config extends NavigationConfig> = {
[Route in keyof Config & string]: Config[Route] extends (
| RouteBuilder<infer Params extends z.ZodSchema, never>
| RouteBuilder<infer Params extends z.ZodSchema, any>
| RouteBuilder<string, infer Params extends z.ZodSchema, never>
| RouteBuilder<string, infer Params extends z.ZodSchema, any>
) ?
Params extends z.ZodSchema ?
Route
Expand All @@ -80,8 +90,8 @@ type RouteWithParams<Config extends NavigationConfig> = {

type RouteWithSearchParams<Config extends NavigationConfig> = {
[Route in keyof Config & string]: Config[Route] extends (
| RouteBuilder<never, infer Search extends z.ZodSchema>
| RouteBuilder<any, infer Search extends z.ZodSchema>
| RouteBuilder<string, never, infer Search extends z.ZodSchema>
| RouteBuilder<string, any, infer Search extends z.ZodSchema>
) ?
Search extends z.ZodSchema ?
Route
Expand All @@ -92,11 +102,12 @@ type RouteWithSearchParams<Config extends NavigationConfig> = {
type SafeNavigation<Config extends NavigationConfig> = {
[Route in keyof Config]: Config[Route] extends (
RouteBuilder<
infer Path extends string,
infer Params extends z.ZodSchema,
infer Search extends z.ZodSchema
>
) ?
SafeRoute<Params, Search>
SafeRoute<Path, Params, Search>
: never;
};

Expand All @@ -108,8 +119,8 @@ type ValidatedRouteParams<
> =
Route extends keyof Pick<Router, AcceptableRoute & keyof Router> ?
Router[Route] extends (
| SafeRoute<infer Params extends z.ZodSchema, any>
| SafeRoute<infer Params extends z.ZodSchema, never>
| SafeRoute<string, infer Params extends z.ZodSchema, any>
| SafeRoute<string, infer Params extends z.ZodSchema, never>
) ?
z.output<Params>
: never
Expand All @@ -123,8 +134,8 @@ type ValidatedRouteSearchParams<
> =
Route extends keyof Pick<Router, AcceptableRoute & keyof Router> ?
Router[Route] extends (
| SafeRoute<any, infer Search extends z.ZodSchema>
| SafeRoute<never, infer Search extends z.ZodSchema>
| SafeRoute<string, any, infer Search extends z.ZodSchema>
| SafeRoute<string, never, infer Search extends z.ZodSchema>
) ?
z.output<Search>
: never
Expand Down
1 change: 0 additions & 1 deletion src/make-route-builder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-secrets/no-secrets */
import { z } from 'zod';

import { makeRouteBuilder } from './make-route-builder';
Expand Down
Loading
Loading