From 28fc7b24c91c538ff09e43bd65f7501474d75415 Mon Sep 17 00:00:00 2001 From: Tate Thurston Date: Sun, 15 Sep 2024 14:17:01 -0700 Subject: [PATCH] Add app directory support (#192) Fixes #170 --- CHANGELOG.md | 23 ++ README.md | 8 +- examples/app/@types/nextjs-routes.d.ts | 15 +- examples/app/src/app/page.tsx | 4 +- examples/cjs/types/nextjs-routes.d.ts | 15 +- examples/intl/@types/nextjs-routes.d.ts | 15 +- examples/typescript/types/nextjs-routes.d.ts | 15 +- packages/e2e/@types/nextjs-routes.d.ts | 15 +- packages/e2e/next-env.d.ts | 2 +- packages/nextjs-routes/package.json | 2 +- .../src/__snapshots__/core.test.ts.snap | 341 +++++++++++++++--- packages/nextjs-routes/src/core.test.ts | 19 +- packages/nextjs-routes/src/core.ts | 50 ++- 13 files changed, 443 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14cd1b4..73751ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 2.2.2 + +- 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: + + ```tsx + import { route } from "nextjs-routes"; + + + Baz + ; + ``` + +- Add `RouteLiteral` type. This type represents a string that confirmed to be a validated application route and can be passed to `Link` or `useRouter`. This is a TypeScript branded type. + + ```ts + import { RouteLiteral } from "nextjs-routes"; + ``` + ## 2.2.1 - Fix route generation on Windows. See [#187](https://github.com/tatethurston/nextjs-routes/issues/187). Thanks @AkanoCA! diff --git a/README.md b/README.md index 27119a1..fe0010c 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,17 @@ ## What is this? 🧐 -`nextjs-routes` makes Next.js's `next/link` and `next/router` routes type safe with zero runtime overhead. `nextjs-routes` scans your `pages` directory and generates route types based on your application's routes. - -`nextjs-routes` drops into your existing Next.js application with minimal configuration. You won't have to change any code, unless it finds some broken links! +`nextjs-routes` generates type safe routing utilities from your `pages` and/or `app` directory. ## Notice -If you are using Next.js's App Router you may not need this library. Next provides an [experimental option to generate typed links](https://nextjs.org/docs/app/building-your-application/configuring/typescript#statically-typed-links). Note that at this time, Next's option only works for the `app` directory, and not `pages`. If you want type safety for `pages`, use this library. +If you are using Next.js's App Router you may not need this library. Next 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 you're using the `app` directory and want to use typed objects instead of string interpolation to provide URL parameters, use this library. ## Highlights 🦄 Zero config -💨 Types only -- zero runtime +💨 Types only -- zero runtime (pages directory only). 🛠 No more broken links diff --git a/examples/app/@types/nextjs-routes.d.ts b/examples/app/@types/nextjs-routes.d.ts index 377030a..31e93c2 100644 --- a/examples/app/@types/nextjs-routes.d.ts +++ b/examples/app/@types/nextjs-routes.d.ts @@ -1,6 +1,6 @@ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -37,12 +37,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -74,7 +81,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route, RouteLiteral } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -89,7 +96,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: StaticRoute | RouteLiteral; locale?: false; } diff --git a/examples/app/src/app/page.tsx b/examples/app/src/app/page.tsx index 5fac4cd..31cfefa 100644 --- a/examples/app/src/app/page.tsx +++ b/examples/app/src/app/page.tsx @@ -1,11 +1,13 @@ import Link from "next/link"; import Client from "./client"; +import { route } from "nextjs-routes"; export default function Page() { return ( <> - + Home + Tate's Store diff --git a/examples/cjs/types/nextjs-routes.d.ts b/examples/cjs/types/nextjs-routes.d.ts index ea309bc..36684ac 100644 --- a/examples/cjs/types/nextjs-routes.d.ts +++ b/examples/cjs/types/nextjs-routes.d.ts @@ -1,6 +1,6 @@ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -37,12 +37,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -74,7 +81,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -89,7 +96,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } diff --git a/examples/intl/@types/nextjs-routes.d.ts b/examples/intl/@types/nextjs-routes.d.ts index 6348fc5..3ec0980 100644 --- a/examples/intl/@types/nextjs-routes.d.ts +++ b/examples/intl/@types/nextjs-routes.d.ts @@ -1,6 +1,6 @@ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -40,12 +40,19 @@ declare module "nextjs-routes" { | "fr" | "nl-NL"; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -81,7 +88,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -96,7 +103,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: Locale | false; } diff --git a/examples/typescript/types/nextjs-routes.d.ts b/examples/typescript/types/nextjs-routes.d.ts index 848ef54..57e964e 100644 --- a/examples/typescript/types/nextjs-routes.d.ts +++ b/examples/typescript/types/nextjs-routes.d.ts @@ -1,6 +1,6 @@ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -39,12 +39,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -76,7 +83,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -91,7 +98,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } diff --git a/packages/e2e/@types/nextjs-routes.d.ts b/packages/e2e/@types/nextjs-routes.d.ts index 81cafed..10bcb65 100644 --- a/packages/e2e/@types/nextjs-routes.d.ts +++ b/packages/e2e/@types/nextjs-routes.d.ts @@ -1,6 +1,6 @@ // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -38,12 +38,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -75,7 +82,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -90,7 +97,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } diff --git a/packages/e2e/next-env.d.ts b/packages/e2e/next-env.d.ts index 4f11a03..a4a7b3f 100644 --- a/packages/e2e/next-env.d.ts +++ b/packages/e2e/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/packages/nextjs-routes/package.json b/packages/nextjs-routes/package.json index 0f299f8..82482a7 100644 --- a/packages/nextjs-routes/package.json +++ b/packages/nextjs-routes/package.json @@ -1,6 +1,6 @@ { "name": "nextjs-routes", - "version": "2.2.1", + "version": "2.2.2-rc.1", "description": "Type safe routing for Next.js", "license": "MIT", "author": "Tate ", diff --git a/packages/nextjs-routes/src/__snapshots__/core.test.ts.snap b/packages/nextjs-routes/src/__snapshots__/core.test.ts.snap index 423472a..6dd63e4 100644 --- a/packages/nextjs-routes/src/__snapshots__/core.test.ts.snap +++ b/packages/nextjs-routes/src/__snapshots__/core.test.ts.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`route generation app directory (experimental) generates routes 1`] = ` +exports[`route generation app directory generates routes 1`] = ` [ [ "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -48,12 +48,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -85,7 +92,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route, RouteLiteral } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -100,7 +107,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: StaticRoute | RouteLiteral; locale?: false; } @@ -184,13 +191,13 @@ declare module "next/router" { ] `; -exports[`route generation app directory (experimental) handles windows paths 1`] = ` +exports[`route generation app directory handles windows paths 1`] = ` [ [ "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -226,12 +233,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -263,7 +277,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route, RouteLiteral } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -278,7 +292,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: StaticRoute | RouteLiteral; locale?: false; } @@ -368,7 +382,7 @@ exports[`route generation configuration i18n 1`] = ` "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -407,12 +421,19 @@ declare module "nextjs-routes" { | "fr" | "nl-NL"; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -448,7 +469,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -463,7 +484,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: Locale | false; } @@ -570,7 +591,7 @@ exports[`route generation configuration outDir 1`] = ` "src/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -606,12 +627,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -643,7 +671,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -658,7 +686,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } @@ -748,7 +776,7 @@ exports[`route generation configuration pageExtensions configured 1`] = ` "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -786,12 +814,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -823,7 +858,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -838,7 +873,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } @@ -928,7 +963,7 @@ exports[`route generation configuration pageExtensions default 1`] = ` "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -964,12 +999,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -1001,7 +1043,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -1016,7 +1058,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } @@ -1106,7 +1148,7 @@ exports[`route generation dedupes 1`] = ` "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -1142,12 +1184,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -1179,7 +1228,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -1194,7 +1243,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } @@ -1284,7 +1333,7 @@ exports[`route generation no routes 1`] = ` "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -1320,12 +1369,206 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + + /** + * A typesafe utility function for generating paths in your application. + * + * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". + */ + export declare function route(r: Route): RouteLiteral; + + /** + * Nearly identical to GetServerSidePropsContext from next, but further narrows + * types based on nextjs-route's route data. + */ + export type GetServerSidePropsContext< + Pathname extends Route["pathname"] = Route["pathname"], + Preview extends NextGetServerSidePropsContext["previewData"] = NextGetServerSidePropsContext["previewData"] + > = Omit & { + params: Extract["query"]; + query: Query; + defaultLocale?: undefined; + locale?: Locale; + locales?: undefined; + }; + + /** + * Nearly identical to GetServerSideProps from next, but further narrows + * types based on nextjs-route's route data. + */ + export type GetServerSideProps< + Props extends { [key: string]: any } = { [key: string]: any }, + Pathname extends Route["pathname"] = Route["pathname"], + Preview extends NextGetServerSideProps["previewData"] = NextGetServerSideProps["previewData"] + > = ( + context: GetServerSidePropsContext + ) => Promise> +} + +// prettier-ignore +declare module "next/link" { + import type { Route } from "nextjs-routes";; + import type { LinkProps as NextLinkProps } from "next/dist/client/link"; + import type { + AnchorHTMLAttributes, + DetailedReactHTMLElement, + MouseEventHandler, + PropsWithChildren, + } from "react"; + export * from "next/dist/client/link"; + + type StaticRoute = Exclude["pathname"]; + + export interface LinkProps + extends Omit, + AnchorHTMLAttributes { + href: Route | StaticRoute | Omit; + locale?: false; + } + + type LinkReactElement = DetailedReactHTMLElement< + { + onMouseEnter?: MouseEventHandler | undefined; + onClick: MouseEventHandler; + href?: string | undefined; + ref?: any; + }, + HTMLElement + >; + + declare function Link(props: PropsWithChildren): LinkReactElement; + + export default Link; +} + +// prettier-ignore +declare module "next/router" { + import type { Locale, Route, RoutedQuery } from "nextjs-routes"; + import type { NextRouter as Router } from "next/dist/client/router"; + export * from "next/dist/client/router"; + export { default } from "next/dist/client/router"; + + type NextTransitionOptions = NonNullable[2]>; + type StaticRoute = Exclude["pathname"]; + + interface TransitionOptions extends Omit { + locale?: false; + } + + type PathnameAndQuery = Required< + Pick, "pathname" | "query"> + >; + + type AutomaticStaticOptimizedQuery = Omit & { + query: Partial; + }; + + type BaseRouter = + | ({ isReady: false } & AutomaticStaticOptimizedQuery) + | ({ isReady: true } & PaQ); + + export type NextRouter

= + BaseRouter> & + Omit< + Router, + | "defaultLocale" + | "domainLocales" + | "isReady" + | "locale" + | "locales" + | "pathname" + | "push" + | "query" + | "replace" + | "route" + > & { + defaultLocale?: undefined; + domainLocales?: undefined; + locale?: Locale; + locales?: undefined; + push( + url: Route | StaticRoute | Omit, + as?: string, + options?: TransitionOptions + ): Promise; + replace( + url: Route | StaticRoute | Omit, + as?: string, + options?: TransitionOptions + ): Promise; + route: P; + }; + + export function useRouter

(): NextRouter

; +} +", + ], +] +`; + +exports[`route generation pages and app directory generates routes 1`] = ` +[ + [ + "@types/nextjs-routes.d.ts", + "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +// This file will be automatically regenerated when your Next.js server is running. +// nextjs-routes version: 2.2.2 +/* eslint-disable */ + +// prettier-ignore +declare module "nextjs-routes" { + import type { + GetServerSidePropsContext as NextGetServerSidePropsContext, + GetServerSidePropsResult as NextGetServerSidePropsResult + } from "next"; + + export type Route = + | StaticRoute<"/"> + | DynamicRoute<"/[foo]", { "foo": string }> + | StaticRoute<"/bar">; + + interface StaticRoute { + pathname: Pathname; + query?: Query | undefined; + hash?: string | null | undefined; + } + + interface DynamicRoute { + pathname: Pathname; + query: Parameters & Query; + hash?: string | null | undefined; + } + + interface Query { + [key: string]: string | string[] | undefined; + }; + + export type RoutedQuery

= Extract< + Route, + { pathname: P } + >["query"]; + + export type Locale = undefined; + + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -1357,7 +1600,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route, RouteLiteral } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -1372,7 +1615,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit | RouteLiteral; locale?: false; } @@ -1462,7 +1705,7 @@ exports[`route generation transforms windows paths 1`] = ` "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -1498,12 +1741,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -1535,7 +1785,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -1550,7 +1800,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } @@ -1640,7 +1890,7 @@ exports[`route generation typescript 1`] = ` "@types/nextjs-routes.d.ts", "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // This file will be automatically regenerated when your Next.js server is running. -// nextjs-routes version: 2.2.1 +// nextjs-routes version: 2.2.2 /* eslint-disable */ // prettier-ignore @@ -1693,12 +1943,19 @@ declare module "nextjs-routes" { export type Locale = undefined; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -1730,7 +1987,7 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + import type { Route } from "nextjs-routes";; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -1745,7 +2002,7 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: Route | StaticRoute | Omit; locale?: false; } diff --git a/packages/nextjs-routes/src/core.test.ts b/packages/nextjs-routes/src/core.test.ts index 25d808e..b83e47e 100644 --- a/packages/nextjs-routes/src/core.test.ts +++ b/packages/nextjs-routes/src/core.test.ts @@ -79,7 +79,7 @@ describe("route generation", () => { expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); }); - describe("app directory (experimental)", () => { + describe("app directory", () => { it("generates routes", () => { // getAppRoutes existsSyncMock @@ -111,6 +111,23 @@ describe("route generation", () => { }); }); + describe("pages and app directory", () => { + it("generates routes", () => { + existsSyncMock + // getPageRoutes + .mockImplementationOnce(() => true) + // getAppRoutes + .mockImplementationOnce(() => true); + findFilesMock + // page routes + .mockReturnValueOnce(["pages/[foo].ts"]) + // app routes + .mockReturnValueOnce(["app/bar/page.ts", "app/page.ts"]); + writeNextJSRoutes({}); + expect(writeFileSyncMock.mock.calls).toMatchSnapshot(); + }); + }); + describe("configuration", () => { describe("pageExtensions", () => { it("default", () => { diff --git a/packages/nextjs-routes/src/core.ts b/packages/nextjs-routes/src/core.ts index 010f9c6..b5e451c 100644 --- a/packages/nextjs-routes/src/core.ts +++ b/packages/nextjs-routes/src/core.ts @@ -7,7 +7,7 @@ import { findFiles, getAppDirectory, getPagesDirectory } from "./utils.js"; // by node 17+ // import pkg from "../package.json" assert { type: "json" }; const pkg = { - version: "2.2.1", + version: "2.2.2", }; type QueryType = "dynamic" | "catch-all" | "optional-catch-all"; @@ -82,7 +82,12 @@ function getQueryInterface( return [`{ ${keys} }`, requiredKeys, optionalCatchAll]; } -function generate(routes: Route[], config: NextJSRoutesOptions): string { +interface GenerateConfig extends NextJSRoutesOptions { + usingAppDirectory: boolean; + usingPagesDirectory: boolean; +} + +function generate(routes: Route[], config: GenerateConfig): string { const i18n = config.i18n ?? { defaultLocale: "", domains: [], @@ -145,12 +150,19 @@ declare module "nextjs-routes" { : `\n | ${i18n.locales.map((x) => `"${x}"`).join("\n | ")}` }; + type Brand = K & { __brand: T }; + + /** + * A string that is a valid application route. + */ + export type RouteLiteral = Brand + /** * A typesafe utility function for generating paths in your application. * * route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar". */ - export declare function route(r: Route): string; + export declare function route(r: Route): RouteLiteral; /** * Nearly identical to GetServerSidePropsContext from next, but further narrows @@ -186,7 +198,13 @@ declare module "nextjs-routes" { // prettier-ignore declare module "next/link" { - import type { Route } from "nextjs-routes"; + ${(() => { + if (config.usingAppDirectory) { + return 'import type { Route, RouteLiteral } from "nextjs-routes";'; + } else { + return 'import type { Route } from "nextjs-routes";'; + } + })()}; import type { LinkProps as NextLinkProps } from "next/dist/client/link"; import type { AnchorHTMLAttributes, @@ -201,7 +219,15 @@ declare module "next/link" { export interface LinkProps extends Omit, AnchorHTMLAttributes { - href: Route | StaticRoute | Omit + href: ${(() => { + if (config.usingPagesDirectory && config.usingAppDirectory) { + return 'Route | StaticRoute | Omit | RouteLiteral'; + } else if (config.usingPagesDirectory) { + return 'Route | StaticRoute | Omit'; + } else { + return "StaticRoute | RouteLiteral"; + } + })()}; locale?: ${!i18n.locales.length ? "false" : `Locale | false`}; } @@ -300,11 +326,6 @@ export const logger: Pick = { }; export interface NextJSRoutesOptions { - /** - * The file path indicating the output directory where the generated route types - * should be written to (e.g.: "types"). - */ - outDir?: string | undefined; /** * Location of the Next.js project. Defaults to the current working directory. * @@ -317,6 +338,11 @@ export interface NextJSRoutesOptions { * const withRoutes = nextRoutes({ dir: __dirname }); */ dir?: string | undefined; + /** + * The file path indicating the output directory where the generated route types + * should be written to (e.g.: "types"). + */ + outDir?: string | undefined; /** * NextJS config option. * https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions @@ -395,6 +421,8 @@ export function getPageRoutes(files: string[], opts: Opts): string[] { export function writeNextJSRoutes(options: NextJSRoutesOptions): void { const defaultOptions = { + usingPagesDirectory: false, + usingAppDirectory: false, dir: process.cwd(), outDir: join(options.dir ?? process.cwd(), "@types"), pageExtensions: ["tsx", "ts", "jsx", "js"], @@ -411,6 +439,7 @@ export function writeNextJSRoutes(options: NextJSRoutesOptions): void { directory: pagesDirectory, }); files.push(...routes); + opts.usingPagesDirectory = true; } const appDirectory = getAppDirectory(opts.dir); if (appDirectory) { @@ -419,6 +448,7 @@ export function writeNextJSRoutes(options: NextJSRoutesOptions): void { directory: appDirectory, }); files.push(...routes); + opts.usingAppDirectory = true; } const outputFilepath = join(opts.outDir, "nextjs-routes.d.ts"); if (opts.outDir && !existsSync(opts.outDir)) {