Repository: tatethurston/nextjs-routes
Pull request #101 (2022-11-21 19:56:08)

add hash support


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b200000..65fbf94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Changelog
 
+## 1.0.3
+
+- The `Route` type now includes `hash`. This enables the following:
+
+  ```tsx
+  // this will link to /foo#bar
+  <Link href={{ pathname: "/foo", hash: "bar" }}>Foo</Link>
+  ```
+
 ## 1.0.2
 
 - `Link` now [accepts anchor props](https://beta.nextjs.org/docs/api-reference/components/link):
diff --git a/README.md b/README.md
index 3dea6db..128c97f 100644
--- a/README.md
+++ b/README.md
@@ -99,7 +99,7 @@ If the route doesn't require any parameters, you can also use a path string:
 
 `useRouter`'s returned router instance types for `push`, `replace` and `query` are now typed based on your application routes.
 
-Identical to `Link`, `push` and `replace` now expect a URL object or path string:
+Identical to `Link`, `push` and `replace` now expect a UrlObject or path string:
 
 #### push
 
diff --git a/examples/intl/nextjs-routes.d.ts b/examples/intl/nextjs-routes.d.ts
index 95b8aea..e7edc61 100644
--- a/examples/intl/nextjs-routes.d.ts
+++ b/examples/intl/nextjs-routes.d.ts
@@ -5,10 +5,22 @@
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/[store]"; query: Query<{ "store": string }> }
-    | { pathname: "/"; query?: Query | undefined };
+    | DynamicRoute<"/[store]", { "store": string }>
+    | StaticRoute<"/">;
 
-  type Query<Params = {}> = Params & {
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -18,7 +30,7 @@ declare module "nextjs-routes" {
   >["query"];
 
   export type Locale = 
-      | "en-US"
+    | "en-US"
     | "fr"
     | "nl-NL";
 
@@ -104,10 +116,10 @@ declare module "next/router" {
         domainLocales?: undefined;
         locale: Locale;
         locales: [
-      "en-US",
-      "fr",
-      "nl-NL"
-    ];
+          "en-US",
+          "fr",
+          "nl-NL"
+        ];
         push(
           url: Route,
           as?: string,
diff --git a/examples/typescript/types/nextjs-routes.d.ts b/examples/typescript/types/nextjs-routes.d.ts
index d185e67..28c7406 100644
--- a/examples/typescript/types/nextjs-routes.d.ts
+++ b/examples/typescript/types/nextjs-routes.d.ts
@@ -5,12 +5,24 @@
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/api/hello"; query?: Query | undefined }
-    | { pathname: "/bars/[bar]"; query: Query<{ "bar": string }> }
-    | { pathname: "/foos/[foo]"; query: Query<{ "foo": string }> }
-    | { pathname: "/"; query?: Query | undefined };
+    | StaticRoute<"/api/hello">
+    | DynamicRoute<"/bars/[bar]", { "bar": string }>
+    | DynamicRoute<"/foos/[foo]", { "foo": string }>
+    | StaticRoute<"/">;
+
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
 
-  type Query<Params = {}> = Params & {
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
diff --git a/src/core.ts b/src/core.ts
index 0104bb6..a0dca71 100644
--- a/src/core.ts
+++ b/src/core.ts
@@ -91,17 +91,27 @@ declare module "nextjs-routes" {
         ? "never"
         : `| ${routes
             .map((route) => {
-              const [query, requiredKeys] = getQueryInterface(route.query);
-              if (requiredKeys > 0) {
-                return `{ pathname: "${route.pathname}"; query: Query<${query}> }`;
-              } else {
-                return `{ pathname: "${route.pathname}"; query?: Query | undefined }`;
-              }
+              const [params, requiredKeys] = getQueryInterface(route.query);
+              return requiredKeys > 0
+                ? `DynamicRoute<"${route.pathname}", ${params}>`
+                : `StaticRoute<"${route.pathname}">`;
             })
             .join("\n    | ")}`
     };
 
-  type Query<Params = {}> = Params & {
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -113,7 +123,7 @@ declare module "nextjs-routes" {
   export type Locale = ${
     !i18n.locales.length
       ? "undefined"
-      : `\n      | ${i18n.locales.map((x) => `"${x}"`).join("\n    | ")}`
+      : `\n    | ${i18n.locales.map((x) => `"${x}"`).join("\n    | ")}`
   };
 
   /**
@@ -198,11 +208,11 @@ declare module "next/router" {
           i18n.defaultLocale ? `: "${i18n.defaultLocale}"` : "?: undefined"
         };
         domainLocales${
-          i18n.domains?.length ? `: ${print(i18n.domains, 4)}` : "?: undefined"
+          i18n.domains?.length ? `: ${print(i18n.domains, 8)}` : "?: undefined"
         };
         locale${!i18n.locales.length ? "?:" : ":"} Locale;
         locales${
-          i18n.locales.length ? `: ${print(i18n.locales, 4)}` : "?: undefined"
+          i18n.locales.length ? `: ${print(i18n.locales, 8)}` : "?: undefined"
         };
         push(
           url: Route,
diff --git a/src/index.ts b/src/index.ts
index 3eea781..2d7f530 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,7 @@
 interface Route {
   pathname: string;
   query?: { [key: string]: string | string[] | undefined };
+  hash?: string | null | undefined;
 }
 
 interface OptionalCatchAll extends Route {
@@ -58,6 +59,7 @@ export function route(r: Route): string {
     }
   }
   const qs = search.toString().length > 0 ? "?" + search.toString() : "";
+  const hash = r.hash ? "#" + r.hash : "";
 
-  return path + qs;
+  return path + qs + hash;
 }


Test patch:
diff --git a/e2e/nextjs-routes.d.ts b/e2e/nextjs-routes.d.ts
index 43b4c1c..c329b5c 100644
--- a/e2e/nextjs-routes.d.ts
+++ b/e2e/nextjs-routes.d.ts
@@ -5,11 +5,23 @@
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/[...slug]"; query: Query<{ "slug": string[] }> }
-    | { pathname: "/foos/[foo]"; query: Query<{ "foo": string }> }
-    | { pathname: "/"; query?: Query | undefined };
+    | DynamicRoute<"/[...slug]", { "slug": string[] }>
+    | DynamicRoute<"/foos/[foo]", { "foo": string }>
+    | StaticRoute<"/">;
+
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
 
-  type Query<Params = {}> = Params & {
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
diff --git a/e2e/typetest.tsx b/e2e/typetest.tsx
index 31d5926..20ac09f 100644
--- a/e2e/typetest.tsx
+++ b/e2e/typetest.tsx
@@ -22,6 +22,7 @@ function expectType<T>(_value: T) {}
 <Link href={{ pathname: "/", query: { bar: "baz" } }} />;
 <Link href={{ pathname: "/", query: { bar: undefined } }} />;
 <Link href={{ pathname: "/", query: { bar: ["baz", "foo"] } }} />;
+<Link href={{ pathname: "/", query: { bar: ["baz", "foo"] }, hash: "foo" }} />;
 
 // Path with dynamic segments
 <Link href={{ pathname: "/foos/[foo]", query: { foo: "baz" } }} />;
@@ -95,6 +96,7 @@ router.push({ pathname: "/", query: undefined });
 router.push({ pathname: "/", query: {} });
 router.push({ pathname: "/", query: { bar: "baz" } });
 router.push({ pathname: "/", query: { bar: ["foo", "baz"] } });
+router.push({ pathname: "/", query: { bar: ["foo", "baz"] }, hash: "foo" });
 
 // Path with dynamic segments
 router.push({ pathname: "/foos/[foo]", query: { foo: "baz" } });
@@ -134,6 +136,7 @@ router.replace({ pathname: "/", query: undefined });
 router.replace({ pathname: "/", query: {} });
 router.replace({ pathname: "/", query: { bar: "baz" } });
 router.replace({ pathname: "/", query: { bar: ["foo", "baz"] } });
+router.replace({ pathname: "/", query: { bar: ["foo", "baz"] }, hash: "foo" });
 
 // Path with dynamic segments
 router.replace({ pathname: "/foos/[foo]", query: { foo: "baz" } });
@@ -215,6 +218,7 @@ r = { pathname: "/foos/[foo]", query: { foo: ["bar", "baz"] } };
 route({ pathname: "/" });
 // Path with dynamic segments
 route({ pathname: "/foos/[foo]", query: { foo: "baz" } });
+route({ pathname: "/foos/[foo]", query: { foo: "baz" }, hash: "foo" });
 // @ts-expect-error missing 'foo' in query
 route({ pathname: "/foos/[foo]", query: { bar: "baz" } });
 // @ts-expect-error 'foo' must be a string, not string[]
diff --git a/src/__snapshots__/core.test.ts.snap b/src/__snapshots__/core.test.ts.snap
index 0b28454..cfc1646 100644
--- a/src/__snapshots__/core.test.ts.snap
+++ b/src/__snapshots__/core.test.ts.snap
@@ -11,10 +11,22 @@ exports[`route generation app directory (experimental) 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/foo"; query?: Query | undefined }
-    | { pathname: "/bar"; query?: Query | undefined };
+    | StaticRoute<"/foo">
+    | StaticRoute<"/bar">;
 
-  type Query<Params = {}> = Params & {
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -158,9 +170,21 @@ exports[`route generation configuration i18n 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/"; query?: Query | undefined };
+    | StaticRoute<"/">;
 
-  type Query<Params = {}> = Params & {
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -170,7 +194,7 @@ declare module "nextjs-routes" {
   >["query"];
 
   export type Locale = 
-      | "en-US"
+    | "en-US"
     | "fr"
     | "nl-NL";
 
@@ -254,25 +278,25 @@ declare module "next/router" {
       > & {
         defaultLocale: "en-US";
         domainLocales: [
-      {
-        "domain": "example.nl",
-        "defaultLocale": "nl-NL",
-        "locales": [
-          "nl-BE"
-        ]
-      },
-      {
-        "domain": "example.fr",
-        "defaultLocale": "fr",
-        "http": true
-      }
-    ];
+          {
+            "domain": "example.nl",
+            "defaultLocale": "nl-NL",
+            "locales": [
+              "nl-BE"
+            ]
+          },
+          {
+            "domain": "example.fr",
+            "defaultLocale": "fr",
+            "http": true
+          }
+        ];
         locale: Locale;
         locales: [
-      "en-US",
-      "fr",
-      "nl-NL"
-    ];
+          "en-US",
+          "fr",
+          "nl-NL"
+        ];
         push(
           url: Route,
           as?: string,
@@ -324,9 +348,21 @@ exports[`route generation configuration outDir 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/404"; query?: Query | undefined };
+    | StaticRoute<"/404">;
+
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
 
-  type Query<Params = {}> = Params & {
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -470,11 +506,23 @@ exports[`route generation configuration pageExtensions configured 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/404"; query?: Query | undefined }
-    | { pathname: "/"; query?: Query | undefined }
-    | { pathname: "/foo"; query?: Query | undefined };
+    | StaticRoute<"/404">
+    | StaticRoute<"/">
+    | StaticRoute<"/foo">;
+
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
 
-  type Query<Params = {}> = Params & {
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -618,9 +666,21 @@ exports[`route generation configuration pageExtensions default 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/404"; query?: Query | undefined };
+    | StaticRoute<"/404">;
+
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
 
-  type Query<Params = {}> = Params & {
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -764,9 +824,21 @@ exports[`route generation dedupes 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/"; query?: Query | undefined };
+    | StaticRoute<"/">;
+
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
 
-  type Query<Params = {}> = Params & {
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -912,7 +984,19 @@ declare module "nextjs-routes" {
   export type Route =
     never;
 
-  type Query<Params = {}> = Params & {
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -1056,9 +1140,21 @@ exports[`route generation transforms windows paths 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "src/pages/[foo]/bar"; query: Query<{ "foo": string }> };
+    | DynamicRoute<"src/pages/[foo]/bar", { "foo": string }>;
 
-  type Query<Params = {}> = Params & {
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
@@ -1202,26 +1298,38 @@ exports[`route generation typescript 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/404"; query?: Query | undefined }
-    | { pathname: "/[foo]"; query: Query<{ "foo": string }> }
-    | { pathname: "/[foo]/[bar]/[baz]"; query: Query<{ "foo": string; "bar": string; "baz": string }> }
-    | { pathname: "/[foo]/bar/[baz]"; query: Query<{ "foo": string; "baz": string }> }
-    | { pathname: "/[foo]/bar/[baz]/foo/[bar]"; query: Query<{ "foo": string; "baz": string; "bar": string }> }
-    | { pathname: "/[foo]/baz"; query: Query<{ "foo": string }> }
-    | { pathname: "/_debug"; query?: Query | undefined }
-    | { pathname: "/_debug/health-check"; query?: Query | undefined }
-    | { pathname: "/api/[[...segments]]"; query?: Query | undefined }
-    | { pathname: "/api/[...segments]"; query: Query<{ "segments": string[] }> }
-    | { pathname: "/api/bar"; query?: Query | undefined }
-    | { pathname: "/foo/[slug]"; query: Query<{ "slug": string }> }
-    | { pathname: "/"; query?: Query | undefined }
-    | { pathname: "/not-found"; query?: Query | undefined }
-    | { pathname: "/settings/bars/[bar]"; query: Query<{ "bar": string }> }
-    | { pathname: "/settings/bars/[bar]/baz"; query: Query<{ "bar": string }> }
-    | { pathname: "/settings/foo"; query?: Query | undefined }
-    | { pathname: "/settings"; query?: Query | undefined };
-
-  type Query<Params = {}> = Params & {
+    | StaticRoute<"/404">
+    | DynamicRoute<"/[foo]", { "foo": string }>
+    | DynamicRoute<"/[foo]/[bar]/[baz]", { "foo": string; "bar": string; "baz": string }>
+    | DynamicRoute<"/[foo]/bar/[baz]", { "foo": string; "baz": string }>
+    | DynamicRoute<"/[foo]/bar/[baz]/foo/[bar]", { "foo": string; "baz": string; "bar": string }>
+    | DynamicRoute<"/[foo]/baz", { "foo": string }>
+    | StaticRoute<"/_debug">
+    | StaticRoute<"/_debug/health-check">
+    | StaticRoute<"/api/[[...segments]]">
+    | DynamicRoute<"/api/[...segments]", { "segments": string[] }>
+    | StaticRoute<"/api/bar">
+    | DynamicRoute<"/foo/[slug]", { "slug": string }>
+    | StaticRoute<"/">
+    | StaticRoute<"/not-found">
+    | DynamicRoute<"/settings/bars/[bar]", { "bar": string }>
+    | DynamicRoute<"/settings/bars/[bar]/baz", { "bar": string }>
+    | StaticRoute<"/settings/foo">
+    | StaticRoute<"/settings">;
+
+  interface StaticRoute<Pathname> {
+    pathname: Pathname;
+    query?: Query | undefined;
+    hash?: string | null | undefined;
+  }
+
+  interface DynamicRoute<Pathname, Parameters> {
+    pathname: Pathname;
+    query: Parameters & Query;
+    hash?: string | null | undefined;
+  }
+
+  interface Query {
     [key: string]: string | string[] | undefined;
   };
 
diff --git a/src/index.test.ts b/src/index.test.ts
index 00b7b18..df5a776 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -3,55 +3,60 @@ import { route } from "./index.js";
 describe(route, () => {
   it("generates paths", () => {
     // static
-    expect(route({ pathname: "/404" })).toMatchInlineSnapshot(`"/404"`);
-    expect(route({ pathname: "/settings/about" })).toMatchInlineSnapshot(
-      `"/settings/about"`
-    );
+    expect(route({ pathname: "/404" })).toEqual("/404");
+    expect(route({ pathname: "/settings/about" })).toEqual("/settings/about");
     // dynamic
-    expect(
-      route({ pathname: "/foos/[foo]", query: { foo: "bar" } })
-    ).toMatchInlineSnapshot(`"/foos/bar"`);
+    expect(route({ pathname: "/foos/[foo]", query: { foo: "bar" } })).toEqual(
+      "/foos/bar"
+    );
     expect(
       route({
         pathname: "/foos/[foo]/bars/[bar]",
         query: { foo: "bar", bar: "baz" },
       })
-    ).toMatchInlineSnapshot(`"/foos/bar/bars/baz"`);
+    ).toEqual("/foos/bar/bars/baz");
     expect(
       route({
         pathname: "/[foo]/[bar]/[baz]",
         query: { foo: "foo", bar: "bar", baz: "baz" },
       })
-    ).toMatchInlineSnapshot(`"/foo/bar/baz"`);
+    ).toEqual("/foo/bar/baz");
     // catch all
     expect(
       route({ pathname: "/[...segments]", query: { segments: ["foo"] } })
-    ).toMatchInlineSnapshot(`"/foo"`);
+    ).toEqual("/foo");
     expect(
       route({ pathname: "/[...segments]", query: { segments: ["foo", "bar"] } })
-    ).toMatchInlineSnapshot(`"/foo/bar"`);
+    ).toEqual("/foo/bar");
     // optional catch all
     expect(
       route({ pathname: "/[[...segments]]", query: { segments: [] } })
-    ).toMatchInlineSnapshot(`"/"`);
+    ).toEqual("/");
     expect(
       route({ pathname: "/[[...segments]]", query: { segments: undefined } })
-    ).toMatchInlineSnapshot(`"/"`);
+    ).toEqual("/");
     expect(
       route({
         pathname: "/[[...segments]]/foos",
         query: { segments: undefined },
       })
-    ).toMatchInlineSnapshot(`"/foos"`);
+    ).toEqual("/foos");
     // query params
     expect(
       route({ pathname: "/foos/[foo]", query: { foo: "foo", bar: "bar" } })
-    ).toMatchInlineSnapshot(`"/foos/foo?bar=bar"`);
+    ).toEqual("/foos/foo?bar=bar");
+    expect(
+      route({
+        pathname: "/foos/[foo]",
+        query: { foo: "foo", bar: "bar", baz: ["1", "2", "3"] },
+      })
+    ).toEqual("/foos/foo?bar=bar&baz=1&baz=2&baz=3");
     expect(
       route({
         pathname: "/foos/[foo]",
         query: { foo: "foo", bar: "bar", baz: ["1", "2", "3"] },
+        hash: "foo",
       })
-    ).toMatchInlineSnapshot(`"/foos/foo?bar=bar&baz=1&baz=2&baz=3"`);
+    ).toEqual("/foos/foo?bar=bar&baz=1&baz=2&baz=3#foo");
   });
 });