Skip to content

Commit

Permalink
feat: Add support for experimental.typedRoutes (#15)
Browse files Browse the repository at this point in the history
* literal routes

* update dependencies

* lint files

* integrate with Next `typedRoutes`

* update docs

* generate changeset

* rename type

* update lockfile

* update ci actions

* remove initial cache from ci script
  • Loading branch information
lukemorales authored Mar 29, 2024
1 parent 5e7e23e commit 31d794e
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 69 deletions.
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

0 comments on commit 31d794e

Please sign in to comment.